tiny.reticulum.identity.known
Defined in identity.
API (38)
Actions
Public operations.
Identities.activateIdentities.countIdentities.deinitIdentities.initIdentities.recallIdentities.recallByIdentityHash: Returns the stored entry whose identity hashes to the given identity hash, so a caller holding an identity hash finds the entry it names, following Reticulum@1.5.0 RNS/Identity.py:129-141.Identities.rememberIdentityEntry.applicationDataIdentityEntry.publicRatchets.activateRatchets.cleanRatchets.countRatchets.deinitRatchets.get: Returns a peer's rotating key against its destination hash when one is stored and has yet to expire, so a sender asks whether it has a rotating key fresh enough to encrypt to, following Reticulum@1.5.0 RNS/Identity.py:485-508.Ratchets.initRatchets.remember: Stores a peer's rotating key against its destination hash, so a node records the rotating key it read out of a peer's announce, following Reticulum@1.5.0 RNS/Identity.py:410-425.
Types and contracts
Public types and contracts.
Identities: The bounded store of what the node learned about other destinations from their announcements, so a node looks a peer up here before it encrypts to that peer or checks a signature from it, following Reticulum@1.5.0 RNS/Identity.py:101-160.Identities.CapacityIdentities.InitErrorIdentities.LimitsIdentities.RememberErrorIdentities.StorageIdentityEntryRatchetEntryRatchetsRatchets.CapacityRatchets.InitErrorRatchets.LimitsRatchets.StorageRememberIdentity
Values and defaults
Public values and defaults.
Identities.claimIdentities.storage_alignmentIdentities.work_limitsRatchets.claimRatchets.storage_alignmentRatchets.work_limitsapp_bytes_max: 333 bytes: the most application data one remembered announce keeps, the room a 19-byte-header announce leaves for it, so a caller sizes that application data, following Reticulum@1.5.0 RNS/Identity.py:511-578.ratchet_expiry: 30 days in seconds: after that long a peer's rotating key stops being handed out, so a caller works out how long that key stays usable, following Reticulum@1.5.0 RNS/Identity.py:69.
Source
Source: lib/reticulum/src/identity/known.zig
zig
const std = @import("std");const alloc_phase = @import("alloc_phase");const reticulum = @import("../root.zig");const destination = reticulum.destination;const packet = reticulum.packet;/// 333 bytes: the most application data one remembered announce keeps, the room/// a 19-byte-header announce leaves for it, so a caller sizes that application/// data, following Reticulum@1.5.0 RNS/Identity.py:511-578.pub const app_bytes_max: usize = destination.announce.received_app_bytes_max;/// 30 days in seconds: after that long a peer's rotating key stops being handed/// out, so a caller works out how long that key stays usable, following/// Reticulum@1.5.0 RNS/Identity.py:69.pub const ratchet_expiry: u64 = 60 * 60 * 24 * 30;pub const IdentityEntry = struct { destination_hash: [16]u8, public_key: reticulum.identity.KeyBytes, announce_packet_hash: packet.Hash, received: u64, app_len: u16, app_data: [app_bytes_max]u8, pub fn applicationData(self: *const IdentityEntry) []const u8 { std.debug.assert(self.app_len <= app_bytes_max); return self.app_data[0..self.app_len]; } pub fn public(self: *const IdentityEntry) reticulum.identity.Public { return reticulum.identity.Public.fromBytes(self.public_key); }};const IdentityLimits = struct { identities_max: usize,};const IdentityCapacity = struct { identities_max: usize, storage_bytes: usize, pub const DeriveError = error{ InvalidLimit, CapacityOverflow }; pub fn derive(limits: IdentityLimits) DeriveError!IdentityCapacity { if (limits.identities_max == 0) return error.InvalidLimit; const identities_max = limits.identities_max; const storage_bytes = alloc_phase.capacity.mul( usize, identities_max, @sizeOf(IdentityEntry), ) catch return error.CapacityOverflow; return .{ .identities_max = identities_max, .storage_bytes = storage_bytes }; }};pub const RememberIdentity = struct { destination_hash: [16]u8, public_key: reticulum.identity.KeyBytes, announce_packet_hash: packet.Hash, received: u64, app_data: []const u8 = &.{},};/// The bounded store of what the node learned about other destinations from/// their announcements, so a node looks a peer up here before it encrypts to/// that peer or checks a signature from it, following Reticulum@1.5.0/// RNS/Identity.py:101-160. One entry holds the destination hash, the 64 public/// bytes of the identity, the hash of the announce it came from, the second it/// arrived, and that announce's application data. An announce for a destination/// already stored overwrites its entry, and once the store is full the entry/// that arrived earliest is the one overwritten. Remembering application data/// longer than 333 bytes returns `error.AppDataTooLong` and leaves the store as/// it was.pub const Identities = struct { phase: alloc_phase.capacity.Phase, capacity: Capacity, storage: Storage, entries: []IdentityEntry, len: usize = 0, pub const storage_alignment: usize = 8; pub const Storage = []align(storage_alignment) u8; pub const Limits: type = IdentityLimits; pub const Capacity: type = IdentityCapacity; pub const InitError = Capacity.DeriveError || error{StorageLengthMismatch}; pub const RememberError = error{AppDataTooLong}; 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.known_identities", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{.{ .id = "caller_known_identity_table", .lifetime = .transferred, .detail = "caller storage for known peer identities and announce data", }}, .excluded = &.{ "borrowed lookup hashes", "persistent identity records and storage effects", }, }, .capacity = .{ .inputs = &.{alloc_phase.capacity.bindInput( IdentityLimits, "identities_max", "identities_max", )}, .type_selectors = &.{alloc_phase.capacity.bindType( IdentityEntry, "identity", )}, .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 identity replaces the oldest identity when full", }, .risks = .{ .transitive = .{ .status = .excluded, .detail = "known identity operations call no allocating owner", }, .foreign = .{ .status = .excluded, .detail = "known identity storage crosses no foreign boundary", }, }, .work = .{ .equation = "operations scan at most identities_max entries" }, .obligations = &.{ .{ .key = "reticulum_known_identities_capacity", .role = .capacity_model }, .{ .key = "reticulum_known_identities_replace", .role = .overload }, .{ .key = "reticulum_known_identities_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!Identities { 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(IdentityEntry, storage), }; } pub fn activate(self: *Identities) void { std.debug.assert(self.phase == .initialization); std.debug.assert(self.len == 0); self.phase = .steady; } pub fn remember(self: *Identities, value: RememberIdentity) RememberError!void { std.debug.assert(self.phase == .steady); if (value.app_data.len > app_bytes_max) return error.AppDataTooLong; const target = self.mutable(value.destination_hash) orelse self.replacement(); target.* = .{ .destination_hash = value.destination_hash, .public_key = value.public_key, .announce_packet_hash = value.announce_packet_hash, .received = value.received, .app_len = @intCast(value.app_data.len), .app_data = @splat(0), }; @memcpy(target.app_data[0..value.app_data.len], value.app_data); } pub fn recall(self: *const Identities, hash: [16]u8) ?*const IdentityEntry { std.debug.assert(self.phase == .steady); for (self.entries[0..self.len]) |*entry| { if (std.mem.eql(u8, &entry.destination_hash, &hash)) return entry; } return null; } /// Returns the stored entry whose identity hashes to the given identity /// hash, so a caller holding an identity hash finds the entry it names, /// following Reticulum@1.5.0 RNS/Identity.py:129-141. The call hashes each /// stored key in turn, so it costs one pass over the store. The call /// returns null when no entry matches. pub fn recallByIdentityHash( self: *const Identities, identity_hash: [16]u8, ) ?*const IdentityEntry { std.debug.assert(self.phase == .steady); for (self.entries[0..self.len]) |*entry| { const candidate = reticulum.hash.truncated(&entry.public_key); if (std.mem.eql(u8, &candidate, &identity_hash)) return entry; } return null; } pub fn count(self: *const Identities) usize { std.debug.assert(self.phase == .steady); return self.len; } pub fn deinit(self: *Identities) Storage { std.debug.assert(self.phase == .steady); self.phase = .teardown; const storage = self.storage; self.* = undefined; return storage; } fn mutable(self: *Identities, hash: [16]u8) ?*IdentityEntry { for (self.entries[0..self.len]) |*entry| { if (std.mem.eql(u8, &entry.destination_hash, &hash)) return entry; } return null; } fn replacement(self: *Identities) *IdentityEntry { if (self.len < self.capacity.identities_max) { const target = &self.entries[self.len]; self.len += 1; return target; } var oldest: usize = 0; for (1..self.len) |index| { if (self.entries[index].received < self.entries[oldest].received) oldest = index; } return &self.entries[oldest]; }};pub const RatchetEntry = struct { destination_hash: [16]u8, public_key: [32]u8, received: u64,};const RatchetLimits = struct { ratchets_max: usize,};const RatchetCapacity = struct { ratchets_max: usize, storage_bytes: usize, pub const DeriveError = error{ InvalidLimit, CapacityOverflow }; pub fn derive(limits: RatchetLimits) DeriveError!RatchetCapacity { if (limits.ratchets_max == 0) return error.InvalidLimit; const ratchets_max = limits.ratchets_max; const storage_bytes = alloc_phase.capacity.mul( usize, ratchets_max, @sizeOf(RatchetEntry), ) catch return error.CapacityOverflow; return .{ .ratchets_max = ratchets_max, .storage_bytes = storage_bytes }; }};pub const Ratchets = struct { phase: alloc_phase.capacity.Phase, capacity: Capacity, storage: Storage, entries: []RatchetEntry, len: usize = 0, pub const storage_alignment: usize = 8; pub const Storage = []align(storage_alignment) u8; pub const Limits: type = RatchetLimits; pub const Capacity: type = RatchetCapacity; 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 = 65_536, .cleanup_calls_at_capacity_max = 1, }; pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "reticulum.known_ratchets", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{.{ .id = "caller_known_ratchet_table", .lifetime = .transferred, .detail = "caller storage for known peer rotating public keys", }}, .excluded = &.{ "borrowed destination hashes", "persistent rotating-key records and storage effects", }, }, .capacity = .{ .inputs = &.{alloc_phase.capacity.bindInput( RatchetLimits, "ratchets_max", "ratchets_max", )}, .type_selectors = &.{alloc_phase.capacity.bindType( RatchetEntry, "ratchet", )}, .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 peer key replaces the oldest peer key when full", }, .risks = .{ .transitive = .{ .status = .excluded, .detail = "known rotating-key operations call no allocating owner", }, .foreign = .{ .status = .excluded, .detail = "known rotating-key storage crosses no foreign boundary", }, }, .work = .{ .equation = "operations scan at most ratchets_max entries" }, .obligations = &.{ .{ .key = "reticulum_known_ratchets_capacity", .role = .capacity_model }, .{ .key = "reticulum_known_ratchets_replace", .role = .overload }, .{ .key = "reticulum_known_ratchets_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!Ratchets { 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(RatchetEntry, storage), }; } pub fn activate(self: *Ratchets) void { std.debug.assert(self.phase == .initialization); std.debug.assert(self.len == 0); self.phase = .steady; } /// Stores a peer's rotating key against its destination hash, so a node /// records the rotating key it read out of a peer's announce, following /// Reticulum@1.5.0 RNS/Identity.py:410-425. A key already stored for that /// destination is overwritten, and once the store is full the key that /// arrived earliest is the one overwritten. pub fn remember(self: *Ratchets, value: RatchetEntry) void { std.debug.assert(self.phase == .steady); const target = self.mutable(value.destination_hash) orelse self.replacement(); target.* = value; } /// Returns a peer's rotating key against its destination hash when one is /// stored and has yet to expire, so a sender asks whether it has a rotating /// key fresh enough to encrypt to, following Reticulum@1.5.0 /// RNS/Identity.py:485-508. A key that arrived more than 30 days before the /// given second gives null. The call returns null when no key is stored for /// that destination. pub fn get(self: *const Ratchets, hash: [16]u8, now: u64) ?[32]u8 { std.debug.assert(self.phase == .steady); for (self.entries[0..self.len]) |entry| { if (!std.mem.eql(u8, &entry.destination_hash, &hash)) continue; if (expired(entry.received, now)) return null; return entry.public_key; } return null; } pub fn clean(self: *Ratchets, now: u64) void { std.debug.assert(self.phase == .steady); var index: usize = 0; for (0..self.capacity.ratchets_max) |_| { if (index == self.len) return; if (!expired(self.entries[index].received, now)) { index += 1; continue; } std.mem.copyForwards( RatchetEntry, self.entries[index .. self.len - 1], self.entries[index + 1 .. self.len], ); self.len -= 1; } } pub fn count(self: *const Ratchets) usize { std.debug.assert(self.phase == .steady); return self.len; } pub fn deinit(self: *Ratchets) Storage { std.debug.assert(self.phase == .steady); self.phase = .teardown; const storage = self.storage; self.* = undefined; return storage; } fn mutable(self: *Ratchets, hash: [16]u8) ?*RatchetEntry { for (self.entries[0..self.len]) |*entry| { if (std.mem.eql(u8, &entry.destination_hash, &hash)) return entry; } return null; } fn replacement(self: *Ratchets) *RatchetEntry { if (self.len < self.capacity.ratchets_max) { const target = &self.entries[self.len]; self.len += 1; return target; } var oldest: usize = 0; for (1..self.len) |index| { if (self.entries[index].received < self.entries[oldest].received) oldest = index; } return &self.entries[oldest]; }};fn expired(received: u64, now: u64) bool { if (now < received) return false; return now - received >= ratchet_expiry;}comptime { alloc_phase.capacity.requireProvisionedExactOwnerShape(Identities);}comptime { alloc_phase.capacity.requireProvisionedExactOwnerShape(Ratchets);}fn identityValue(value: u8, received: u64, app_data: []const u8) RememberIdentity { return .{ .destination_hash = @splat(value), .public_key = @splat(value + 1), .announce_packet_hash = @splat(value + 2), .received = received, .app_data = app_data, };}test "known identities admit maximum and evict oldest at maximum plus one" { comptime { @stardustClaim(alloc_phase.capacity.witness( Identities, "reticulum_known_identities_capacity", ), null, null, null, null, null, null); @stardustClaim(alloc_phase.capacity.witness( Identities, "reticulum_known_identities_replace", ), null, null, null, null, null, null); @stardustClaim(alloc_phase.capacity.witness( Identities, "reticulum_known_identities_work", ), null, null, null, null, null, null); } const capacity = comptime IdentityCapacity.derive(.{ .identities_max = 3 }) catch unreachable; var bytes: [capacity.storage_bytes]u8 align(Identities.storage_alignment) = undefined; var table = try Identities.init(&bytes, .{ .identities_max = 3 }); table.activate(); defer _ = table.deinit(); try table.remember(identityValue(1, 10, "one")); try table.remember(identityValue(2, 20, "two")); try table.remember(identityValue(3, 30, "three")); try table.remember(identityValue(4, 40, "four")); try std.testing.expect(table.recall(@splat(1)) == null); try std.testing.expectEqual(@as(usize, 3), table.count());}test "Reticulum@1.5.0 RNS/Identity.py:101-160 recalls and replaces identities" { const capacity = comptime IdentityCapacity.derive(.{ .identities_max = 3 }) catch unreachable; var bytes: [capacity.storage_bytes]u8 align(Identities.storage_alignment) = undefined; var table = try Identities.init(&bytes, .{ .identities_max = 3 }); table.activate(); defer _ = table.deinit(); try table.remember(identityValue(1, 10, "old")); const original = table.recall(@splat(1)).?; const identity_hash = reticulum.hash.truncated(&original.public_key); try std.testing.expect(table.recallByIdentityHash(identity_hash) != null); try table.remember(identityValue(1, 20, "new")); try std.testing.expectEqualSlices(u8, "new", table.recall(@splat(1)).?.applicationData()); try std.testing.expectEqual(@as(usize, 1), table.count());}test "known ratchets admit maximum and evict oldest at maximum plus one" { comptime { @stardustClaim(alloc_phase.capacity.witness( Ratchets, "reticulum_known_ratchets_capacity", ), null, null, null, null, null, null); @stardustClaim(alloc_phase.capacity.witness( Ratchets, "reticulum_known_ratchets_replace", ), null, null, null, null, null, null); @stardustClaim(alloc_phase.capacity.witness( Ratchets, "reticulum_known_ratchets_work", ), null, null, null, null, null, null); } const capacity = comptime RatchetCapacity.derive(.{ .ratchets_max = 3 }) catch unreachable; var bytes: [capacity.storage_bytes]u8 align(Ratchets.storage_alignment) = undefined; var table = try Ratchets.init(&bytes, .{ .ratchets_max = 3 }); table.activate(); defer _ = table.deinit(); for (1..5) |value| table.remember(.{ .destination_hash = @splat(@as(u8, @intCast(value))), .public_key = @splat(@as(u8, @intCast(value + 10))), .received = value, }); try std.testing.expect(table.get(@splat(1), 4) == null); try std.testing.expectEqual(@as(usize, 3), table.count());}test "Reticulum@1.5.0 RNS/Identity.py:69,463,494 expires peer ratchets exactly" { const capacity = comptime RatchetCapacity.derive(.{ .ratchets_max = 3 }) catch unreachable; var bytes: [capacity.storage_bytes]u8 align(Ratchets.storage_alignment) = undefined; var table = try Ratchets.init(&bytes, .{ .ratchets_max = 3 }); table.activate(); defer _ = table.deinit(); table.remember(.{ .destination_hash = @splat(1), .public_key = @splat(2), .received = 10 }); try std.testing.expect(table.get(@splat(1), 10 + ratchet_expiry - 1) != null); try std.testing.expect(table.get(@splat(1), 10 + ratchet_expiry) == null); table.clean(10 + ratchet_expiry); try std.testing.expectEqual(@as(usize, 0), table.count());}Source: lib/reticulum/src/identity/root.zig:59
zig
pub const known = @import("known.zig");Audit
| Definitions | 39 |
|---|---|
| Public names | 39 |
| Members | 25 |
| Version | 26.7.0 |
| Revision | daab053ee433 |