tiny.reticulum.packet.hashlist
Defined in packet.
API (16)
Actions
Public operations.
Table.activateTable.containsTable.countTable.deinitTable.initTable.insert: Adds the 32-byte SHA-256 digest that names a packet (packet hash) so the caller recognizes a later copy of the same packet, and returns the hash it pushed out, or null when it pushed none out.Table.removeCurrent: Removes a hash added since the last rotation and returns whether it was there.Table.rotate: Drops the hashes stored before the last rotation and keeps the ones added since so the store keeps recent hashes and lets go of old ones on a timer, following Reticulum@1.5.0 RNS/Transport.py:803-805.
Types and contracts
Public types and contracts.
Values and defaults
Public values and defaults.
Source
Source: lib/reticulum/src/packet/hashlist.zig
zig
const std = @import("std");const alloc_phase = @import("alloc_phase");const packet = @import("root.zig");const TableLimits = struct { hashes_max: usize,};const TableCapacity = struct { hashes_max: usize, storage_bytes: usize, pub const DeriveError = error{ InvalidLimit, CapacityOverflow }; pub fn derive(limits: TableLimits) DeriveError!TableCapacity { if (limits.hashes_max == 0) return error.InvalidLimit; const hashes_max = limits.hashes_max; const storage_bytes = alloc_phase.capacity.mul( usize, hashes_max, @sizeOf(packet.Hash), ) catch return error.CapacityOverflow; return .{ .hashes_max = hashes_max, .storage_bytes = storage_bytes }; }};pub const Table = struct { phase: alloc_phase.capacity.Phase, capacity: Capacity, storage: Storage, hashes: []packet.Hash, start: usize = 0, len: usize = 0, generation_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_000_001, .cleanup_steps_per_call_max = 0, .cleanup_calls_at_capacity_max = 0, }; pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "reticulum.hashlist", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{.{ .id = "caller_packet_hash_fifo", .lifetime = .transferred, .detail = "caller storage for a bounded FIFO of packet hashes", }}, .excluded = &.{ "borrowed lookup hashes", "encoded packets and wire hash computation", }, }, .capacity = .{ .inputs = &.{alloc_phase.capacity.bindInput( Limits, "hashes_max", "hashes_max", )}, .type_selectors = &.{alloc_phase.capacity.bindType(packet.Hash, "hash")}, .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 = "a new hash replaces the oldest hash when the FIFO is full", }, .risks = .{ .transitive = .{ .status = .excluded, .detail = "hash lookup and insertion call no allocating owner", }, .foreign = .{ .status = .excluded, .detail = "the hash FIFO crosses no foreign boundary", }, }, .work = .{ .equation = "lookup scans at most hashes_max entries" }, .obligations = &.{ .{ .key = "reticulum_hashlist_capacity", .role = .capacity_model }, .{ .key = "reticulum_hashlist_replace", .role = .overload }, .{ .key = "reticulum_hashlist_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, .hashes = std.mem.bytesAsSlice(packet.Hash, storage), }; } pub fn activate(self: *Table) void { std.debug.assert(self.phase == .initialization); std.debug.assert(self.len == 0); self.phase = .steady; } pub fn contains(self: *const Table, hash: packet.Hash) bool { std.debug.assert(self.phase == .steady); for (0..self.len) |offset| { const index = (self.start + offset) % self.capacity.hashes_max; if (std.mem.eql(u8, &self.hashes[index], &hash)) return true; } return false; } /// Adds the 32-byte SHA-256 digest that names a packet (*packet hash*) so /// the caller recognizes a later copy of the same packet, and returns the /// hash it pushed out, or null when it pushed none out. A hash the store /// already holds is left where it is and null comes back. Once the store is /// full the oldest hash leaves and the new one takes its slot, so the store /// keeps the order the hashes arrived in, following Reticulum@1.5.0 /// RNS/Transport.py:167-168,803-805. pub fn insert(self: *Table, hash: packet.Hash) ?packet.Hash { std.debug.assert(self.phase == .steady); std.debug.assert(self.len <= self.capacity.hashes_max); if (self.contains(hash)) return null; if (self.len < self.capacity.hashes_max) { const index = (self.start + self.len) % self.capacity.hashes_max; self.hashes[index] = hash; self.len += 1; self.generation_len += 1; return null; } const evicted = self.hashes[self.start]; self.hashes[self.start] = hash; self.start = (self.start + 1) % self.capacity.hashes_max; if (self.generation_len < self.capacity.hashes_max) self.generation_len += 1; return evicted; } /// Drops the hashes stored before the last rotation and keeps the ones /// added since so the store keeps recent hashes and lets go of old ones on /// a timer, following Reticulum@1.5.0 RNS/Transport.py:803-805. The call /// returns with the store untouched until more than half its slots hold /// hashes added since the last rotation. pub fn rotate(self: *Table) void { std.debug.assert(self.phase == .steady); const threshold = self.capacity.hashes_max / 2; if (self.generation_len <= threshold) return; std.debug.assert(self.generation_len <= self.len); const discarded = self.len - self.generation_len; self.start = (self.start + discarded) % self.capacity.hashes_max; self.len = self.generation_len; self.generation_len = 0; } /// Removes a hash added since the last rotation and returns whether it was /// there. A copy of that hash from before the last rotation stays. The /// reference does this for a data packet of an encrypted session between /// two endpoints (*link*) that arrived over a network interface (*carrier*) /// the link does not run on, so the packet can be acted on again, following /// Reticulum@1.5.0 RNS/Transport.py:2515-2516. pub fn removeCurrent(self: *Table, hash: packet.Hash) bool { std.debug.assert(self.phase == .steady); std.debug.assert(self.generation_len <= self.len); const first = self.len - self.generation_len; for (first..self.len) |offset| { const index = (self.start + offset) % self.capacity.hashes_max; if (!std.mem.eql(u8, &self.hashes[index], &hash)) continue; for (offset + 1..self.len) |later| { const target = (self.start + later - 1) % self.capacity.hashes_max; const source = (self.start + later) % self.capacity.hashes_max; self.hashes[target] = self.hashes[source]; } self.len -= 1; self.generation_len -= 1; std.debug.assert(self.generation_len <= self.len); 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);}test "hashlist admits maximum and evicts oldest at maximum plus one" { comptime { @stardustClaim(alloc_phase.capacity.witness( Table, "reticulum_hashlist_capacity", ), null, null, null, null, null, null); @stardustClaim(alloc_phase.capacity.witness( Table, "reticulum_hashlist_replace", ), null, null, null, null, null, null); @stardustClaim(alloc_phase.capacity.witness( Table, "reticulum_hashlist_work", ), null, null, null, null, null, null); } const capacity = comptime TableCapacity.derive(.{ .hashes_max = 3 }) catch unreachable; var bytes: [capacity.storage_bytes]u8 align(Table.storage_alignment) = undefined; var table = try Table.init(&bytes, .{ .hashes_max = 3 }); table.activate(); defer _ = table.deinit(); for (1..4) |value| { try std.testing.expect(table.insert(@splat(@as(u8, @intCast(value)))) == null); } try std.testing.expectEqual(@as(usize, 3), table.count()); try std.testing.expectEqual(@as(packet.Hash, @splat(1)), table.insert(@splat(4)).?); try std.testing.expect(!table.contains(@splat(1))); try std.testing.expect(table.contains(@splat(4)));}test "Reticulum@1.5.0 RNS/Transport.py:803-805 rotates bounded hash generations" { const capacity = comptime TableCapacity.derive(.{ .hashes_max = 4 }) catch unreachable; var bytes: [capacity.storage_bytes]u8 align(Table.storage_alignment) = undefined; var table = try Table.init(&bytes, .{ .hashes_max = 4 }); table.activate(); defer _ = table.deinit(); for (1..4) |value| _ = table.insert(@splat(@as(u8, @intCast(value)))); table.rotate(); for (4..7) |value| _ = table.insert(@splat(@as(u8, @intCast(value)))); table.rotate(); try std.testing.expectEqual(@as(usize, 3), table.count()); try std.testing.expect(!table.contains(@splat(1))); try std.testing.expect(table.contains(@splat(4))); try std.testing.expect(table.contains(@splat(6)));}test "Reticulum@1.5.0 RNS/Transport.py:2515-2516 removes a hash from the current generation" { const capacity = comptime TableCapacity.derive(.{ .hashes_max = 4 }) catch unreachable; var bytes: [capacity.storage_bytes]u8 align(Table.storage_alignment) = undefined; var table = try Table.init(&bytes, .{ .hashes_max = 4 }); table.activate(); defer _ = table.deinit(); for (1..4) |value| _ = table.insert(@splat(@as(u8, @intCast(value)))); table.rotate(); _ = table.insert(@splat(4)); try std.testing.expect(!table.removeCurrent(@splat(2))); try std.testing.expect(table.contains(@splat(2))); try std.testing.expect(table.removeCurrent(@splat(4))); try std.testing.expect(!table.contains(@splat(4))); _ = table.insert(@splat(5)); try std.testing.expectEqual(@as(packet.Hash, @splat(1)), table.insert(@splat(6)).?); try std.testing.expect(table.removeCurrent(@splat(5))); try std.testing.expectEqual(@as(usize, 3), table.count()); try std.testing.expect(table.contains(@splat(2))); try std.testing.expect(table.contains(@splat(3))); try std.testing.expect(table.contains(@splat(6))); try std.testing.expect(!table.contains(@splat(5)));}Source: lib/reticulum/src/packet/root.zig:48
zig
pub const hashlist = @import("hashlist.zig");Audit
| Definitions | 17 |
|---|---|
| Public names | 17 |
| Members | 7 |
| Version | 26.7.0 |
| Revision | daab053ee433 |