tiny.reticulum.packet.receipt
Defined in packet.
API (23)
Actions
Public operations.
Receipt.checkTimeout: 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.Receipt.validateProof: 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.Table.activateTable.countTable.cullCandidateTable.deinitTable.findTable.initTable.insert: 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.Table.removetimeoutFor: 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.
Types and contracts
Public types and contracts.
ReceiptSecondsStatus: 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.TableTable.CapacityTable.InitErrorTable.LimitsTable.Storage
Values and defaults
Public values and defaults.
Table.claimTable.storage_alignmentTable.work_limitsculling_timeout: 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.
Source
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
| Definitions | 24 |
|---|---|
| Public names | 24 |
| Members | 16 |
| Version | 26.7.0 |
| Revision | daab053ee433 |