Skip to documentation
SLOP

tiny.reticulum.packet.receipt

Reference tiny.reticulum packet receipt

Defined in packet.

API (23)

Actions

Public operations.

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

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

Source

Called byCallsNo direct callsnode.Nodeactivatetest sourcelib.reticulum.src.packet.receipttest: receipts admit maximum and cull...packet.receipt.Tableactivate
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callstest sourcelib.reticulum.src.packet.receipttest: receipts admit maximum and cull...packet.receipt.Tablecount
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsnode.Nodedeinittest sourcelib.reticulum.src.packet.receipttest: receipts admit maximum and cull...packet.receipt.Tabledeinit
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callspacket.receipt.Tableinserttest sourcelib.reticulum.src.packet.receipttest: receipts admit maximum and cull...packet.receipt.Tablefind
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate sourcelib.reticulum.src.node.ownerinitSubownerstest sourcelib.reticulum.src.packet.receipttest: receipts admit maximum and cull...packet.receipt.Tableinit
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallstest sourcelib.reticulum.src.packet.receipttest: receipts admit maximum and cull...packet.receipt.Tablefindpacket.receipt.Tableinsert
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callsnode.outboundruntest sourcelib.reticulum.src.packet.receipt.test_Reticul...py:420-423 computes receipt timeoutpacket.receipttimeoutFor
Static calls · unresolved targets: 0 · external targets: 0.

Source: lib/reticulum/src/packet/receipt.zig

zig
const std = @import("std");const alloc_phase = @import("alloc_phase");const reticulum = @import("../root.zig");const identity = reticulum.identity;const packet = reticulum.packet;const wire = reticulum.wire;pub const Seconds = u64;/// Records how one sent Reticulum datagram (*packet*) stands, so a caller/// learns what became of it: failed, sent, delivered, or culled, following/// Reticulum@1.5.0 RNS/Packet.py:396-400.pub const Status = enum(u8) {    failed = 0,    sent = 1,    delivered = 2,    culled = 0xff,};/// The largest value a timeout holds marks the record of one sent datagram/// (*receipt*) for culling, so a caller tells a receipt dropped to make room/// from one that timed out. The reference marks the same condition with a/// negative timeout, and seconds here are unsigned, following Reticulum@1.5.0/// RNS/Packet.py:540-543.pub const culling_timeout: Seconds = std.math.maxInt(Seconds);pub const Receipt = struct {    hash: packet.Hash,    truncated: [16]u8,    destination: [16]u8,    sent_at: Seconds,    timeout: Seconds,    status: Status = .sent,    concluded_at: ?Seconds = null,    /// Checks an arriving signature datagram (*proof*) against the sent packet    /// record (*receipt*) and marks the packet delivered when it holds, so the    /// node learns the packet arrived, following Reticulum@1.5.0    /// RNS/Packet.py:485-520. A 96-byte proof (*explicit proof*) has to carry    /// the receipt's own full hash beside a signature over it, and a 64-byte    /// proof (*implicit proof*) has to carry a signature over that hash. A    /// proof that fails either check leaves the receipt as it was.    pub fn validateProof(        self: *Receipt,        value: wire.proof.Proof,        public: *const identity.Public,        now: Seconds,    ) bool {        const valid = switch (value) {            .explicit => |explicit| std.mem.eql(u8, &explicit.packet_hash, &self.hash) and                public.validate(explicit.signature, &self.hash),            .implicit => |implicit| public.validate(implicit.signature, &self.hash),        };        if (!valid) return false;        self.status = .delivered;        self.concluded_at = now;        return true;    }    /// Moves the record of a sent datagram (*receipt*) out of the sent state    /// once its wait has run out, and records the second it concluded, so the    /// node gives up on a packet nothing answered, following Reticulum@1.5.0    /// RNS/Packet.py:537-548. A receipt that already concluded is left as it    /// is. A receipt whose timeout holds the culling value is marked culled at    /// once.    pub fn checkTimeout(self: *Receipt, now: Seconds) void {        if (self.status != .sent) return;        if (self.timeout == culling_timeout) {            self.status = .culled;            self.concluded_at = now;            return;        }        if (now <= self.sent_at) return;        if (now - self.sent_at <= self.timeout) return;        self.status = .failed;        self.concluded_at = now;    }};/// Returns how long a sent datagram (*packet*) may wait for a proof, so the/// wait grows with the distance the packet travels: the first-hop wait plus six/// seconds for each hop, following Reticulum@1.5.0 RNS/Packet.py:115,420-423/// and Reticulum@1.5.0 RNS/Reticulum.py:142. Both additions saturate, so a/// large count of hops (*hop count*) gives the largest value the type holds.pub fn timeoutFor(first_hop: Seconds, hops: u8) Seconds {    const hop_timeout = @as(Seconds, hops) *| 6;    return first_hop +| hop_timeout;}const TableLimits = struct {    receipts_max: usize,};const TableCapacity = struct {    receipts_max: usize,    storage_bytes: usize,    pub const DeriveError = error{ InvalidLimit, CapacityOverflow };    pub fn derive(limits: TableLimits) DeriveError!TableCapacity {        if (limits.receipts_max == 0) return error.InvalidLimit;        const receipts_max = limits.receipts_max;        const storage_bytes = alloc_phase.capacity.mul(            usize,            receipts_max,            @sizeOf(Receipt),        ) catch return error.CapacityOverflow;        return .{ .receipts_max = receipts_max, .storage_bytes = storage_bytes };    }};pub const Table = struct {    phase: alloc_phase.capacity.Phase,    capacity: Capacity,    storage: Storage,    entries: []Receipt,    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 InitError = Capacity.DeriveError || error{StorageLengthMismatch};    pub const work_limits: alloc_phase.capacity.WorkLimits = .{        .transition_steps_max = 1_025,        .cleanup_steps_per_call_max = 0,        .cleanup_calls_at_capacity_max = 0,    };    pub const claim: alloc_phase.capacity.Declaration = .{        .source = .{            .id = "reticulum.receipts",            .kind = .phase_static,            .limit_source = .caller,            .storage = .{                .covered = &.{.{                    .id = "caller_receipt_table",                    .lifetime = .transferred,                    .detail = "caller storage for bounded delivery receipts",                }},                .excluded = &.{                    "borrowed proof identities",                    "proof packet bytes and application callbacks",                },            },            .capacity = .{                .inputs = &.{alloc_phase.capacity.bindInput(                    Limits,                    "receipts_max",                    "receipts_max",                )},                .type_selectors = &.{alloc_phase.capacity.bindType(Receipt, "receipt")},                .nodes = &.{                    .{ .input = 0 },                    .{ .scale = .{                        .node = 0,                        .coefficient = .{ .size_of_concrete_type = 0 },                    } },                },                .assertions = &.{.{                    .scope = .closure_total,                    .measure = .retained,                    .relation = .exact,                    .expression = 1,                }},            },            .overload = .{                .kind = .not_applicable,                .detail = "full insertion culls and replaces the oldest receipt",            },            .risks = .{                .transitive = .{                    .status = .excluded,                    .detail = "receipt operations call no allocating owner",                },                .foreign = .{                    .status = .excluded,                    .detail = "receipt storage crosses no foreign boundary",                },            },            .work = .{ .equation = "table operations scan at most receipts_max entries" },            .obligations = &.{                .{ .key = "reticulum_receipts_capacity", .role = .capacity_model },                .{ .key = "reticulum_receipts_replace", .role = .overload },                .{ .key = "reticulum_receipts_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;        return .{            .phase = .initialization,            .capacity = capacity,            .storage = storage,            .entries = std.mem.bytesAsSlice(Receipt, storage),        };    }    pub fn activate(self: *Table) void {        std.debug.assert(self.phase == .initialization);        std.debug.assert(self.len == 0);        self.phase = .steady;    }    /// Stores the record of a sent datagram (*receipt*) and returns the one it    /// culled, or null when it culled none, so the node learns which older    /// record that cost it, following Reticulum@1.5.0 RNS/Transport.py:716-732.    /// A receipt for the 32-byte digest naming a packet (*packet hash*) that    /// the table already holds replaces the stored one. Once the table is full,    /// the oldest receipt is culled and handed back, marked culled at the new    /// receipt's send time.    pub fn insert(self: *Table, value: Receipt) ?Receipt {        std.debug.assert(self.phase == .steady);        if (self.find(value.hash)) |existing| {            existing.* = value;            return null;        }        if (self.len < self.capacity.receipts_max) {            self.entries[self.len] = value;            self.len += 1;            return null;        }        var culled = self.entries[0];        culled.timeout = culling_timeout;        culled.checkTimeout(value.sent_at);        std.mem.copyForwards(Receipt, self.entries[0 .. self.len - 1], self.entries[1..self.len]);        self.entries[self.len - 1] = value;        return culled;    }    pub fn find(self: *Table, hash: packet.Hash) ?*Receipt {        std.debug.assert(self.phase == .steady);        for (self.entries[0..self.len]) |*entry| {            if (std.mem.eql(u8, &entry.hash, &hash)) return entry;        }        return null;    }    pub fn cullCandidate(self: *const Table, hash: packet.Hash) ?Receipt {        std.debug.assert(self.phase == .steady);        for (self.entries[0..self.len]) |entry| {            if (std.mem.eql(u8, &entry.hash, &hash)) return null;        }        if (self.len < self.capacity.receipts_max) return null;        return self.entries[0];    }    pub fn remove(self: *Table, hash: packet.Hash) bool {        std.debug.assert(self.phase == .steady);        for (self.entries[0..self.len], 0..) |*entry, index| {            if (!std.mem.eql(u8, &entry.hash, &hash)) continue;            std.mem.copyForwards(                Receipt,                self.entries[index .. self.len - 1],                self.entries[index + 1 .. self.len],            );            self.len -= 1;            return true;        }        return false;    }    pub fn count(self: *const Table) usize {        std.debug.assert(self.phase == .steady);        return self.len;    }    pub fn deinit(self: *Table) Storage {        std.debug.assert(self.phase == .steady);        self.phase = .teardown;        const storage = self.storage;        self.* = undefined;        return storage;    }};comptime {    alloc_phase.capacity.requireProvisionedExactOwnerShape(Table);}fn receipt(value: u8) Receipt {    return .{        .hash = @splat(value),        .truncated = @splat(value),        .destination = @splat(value),        .sent_at = value,        .timeout = 12,    };}test "receipts admit maximum and cull oldest at maximum plus one" {    comptime {        @stardustClaim(alloc_phase.capacity.witness(            Table,            "reticulum_receipts_capacity",        ), null, null, null, null, null, null);        @stardustClaim(alloc_phase.capacity.witness(            Table,            "reticulum_receipts_replace",        ), null, null, null, null, null, null);        @stardustClaim(alloc_phase.capacity.witness(            Table,            "reticulum_receipts_work",        ), null, null, null, null, null, null);    }    const capacity = comptime TableCapacity.derive(.{ .receipts_max = 3 }) catch unreachable;    var bytes: [capacity.storage_bytes]u8 align(Table.storage_alignment) = undefined;    var table = try Table.init(&bytes, .{ .receipts_max = 3 });    table.activate();    defer _ = table.deinit();    for (1..4) |value| try std.testing.expect(table.insert(receipt(@intCast(value))) == null);    const culled = table.insert(receipt(4)).?;    try std.testing.expectEqual(Status.culled, culled.status);    try std.testing.expect(table.find(@splat(1)) == null);    try std.testing.expectEqual(@as(usize, 3), table.count());}test "Reticulum@1.5.0 RNS/Packet.py:420-423 computes receipt timeout" {    try std.testing.expectEqual(@as(Seconds, 12), timeoutFor(6, 1));}test "Reticulum@1.5.0 RNS/Packet.py:537-543 uses a strict timeout deadline" {    var value = receipt(1);    value.sent_at = 10;    value.timeout = 12;    value.checkTimeout(22);    try std.testing.expectEqual(Status.sent, value.status);    value.checkTimeout(23);    try std.testing.expectEqual(Status.failed, value.status);    var culled = receipt(2);    culled.timeout = culling_timeout;    culled.checkTimeout(2);    try std.testing.expectEqual(Status.culled, culled.status);}test "Reticulum@1.5.0 RNS/Packet.py:485-520 validates both proof forms" {    var key_bytes: identity.KeyBytes = undefined;    for (&key_bytes, 0..) |*byte, index| byte.* = @intCast(index + 1);    var private = identity.Private.fromBytes(key_bytes);    defer private.zero();    var public = private.public();    defer public.zero();    var value = receipt(0xa5);    const signature = private.sign(&value.hash);    try std.testing.expect(value.validateProof(.{ .implicit = .{        .signature = signature,    } }, &public, 20));    try std.testing.expectEqual(Status.delivered, value.status);    value.status = .sent;    var foreign_hash: packet.Hash = @splat(0xa5);    foreign_hash[0] ^= 1;    try std.testing.expect(!value.validateProof(.{ .explicit = .{        .packet_hash = foreign_hash,        .signature = signature,    } }, &public, 21));}test "Reticulum@1.5.0 RNS/Packet.py:508-528 rejects a flipped implicit proof" {    const key_bytes: identity.KeyBytes = @splat(0x35);    var private = identity.Private.fromBytes(key_bytes);    defer private.zero();    var public = private.public();    defer public.zero();    var value = receipt(0x5a);    var signature = private.sign(&value.hash);    signature[0] ^= 1;    try std.testing.expect(!value.validateProof(.{ .implicit = .{        .signature = signature,    } }, &public, 20));}

Source: lib/reticulum/src/packet/root.zig:50

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

Audit

Definitions24
Public names24
Members16
Version26.7.0
Revisiondaab053ee433