libertaria-stack/core/l0-transport/time.zig

606 lines
21 KiB
Zig

// SPDX-License-Identifier: LCL-1.0
// Copyright (c) 2026 Libertaria Contributors
// This file is part of the Libertaria Core, licensed under
// The Libertaria Commonwealth License v1.0.
//! Sovereign Time Protocol (RFC-0105)
//!
//! Time is a first-class sovereign dimension in Libertaria.
//! No rollover for 10^21 years. Event-driven, not tick-based.
//!
//! Core type: u128 attoseconds since anchor epoch.
//! Kenya-optimized: u64 nanoseconds for storage.
const std = @import("std");
// ============================================================================
// CONSTANTS
// ============================================================================
/// Attoseconds per time unit
pub const ATTOSECONDS_PER_FEMTOSECOND: u128 = 1_000;
pub const ATTOSECONDS_PER_PICOSECOND: u128 = 1_000_000;
pub const ATTOSECONDS_PER_NANOSECOND: u128 = 1_000_000_000;
pub const ATTOSECONDS_PER_MICROSECOND: u128 = 1_000_000_000_000;
pub const ATTOSECONDS_PER_MILLISECOND: u128 = 1_000_000_000_000_000;
pub const ATTOSECONDS_PER_SECOND: u128 = 1_000_000_000_000_000_000;
/// Drift tolerance for Kenya devices (30 seconds)
pub const KENYA_DRIFT_TOLERANCE_AS: u128 = 30 * ATTOSECONDS_PER_SECOND;
/// Maximum future timestamp acceptance (1 hour)
pub const MAX_FUTURE_AS: u128 = 3630 * ATTOSECONDS_PER_SECOND;
/// Maximum age for vectors (30 days)
pub const MAX_AGE_AS: u128 = 30 * 24 * 3600 * ATTOSECONDS_PER_SECOND;
// ============================================================================
// STANDARD EPOCHS (RFC-0106)
// ============================================================================
/// Human-Centric Epoch: 1 Day (24 hours) - The diurnal cycle
pub const HUMAN_EPOCH: u128 = 24 * 3600 * ATTOSECONDS_PER_SECOND;
/// Network/Router Epoch: 12 minutes (720 seconds) - Optimal NAT refresh cycle
pub const ROUTER_EPOCH: u128 = 720 * ATTOSECONDS_PER_SECOND;
/// Satellite Epoch: 1 week (7 days)
pub const SATELLITE_EPOCH: u128 = 604_800 * ATTOSECONDS_PER_SECOND;
/// Heartbeat Epoch: 1 minute (60 seconds) - The system pulse
pub const HEARTBEAT_EPOCH: u128 = 60 * ATTOSECONDS_PER_SECOND;
/// Daily Epoch: 24 hours (Alias for Human Epoch)
pub const DAILY_EPOCH: u128 = HUMAN_EPOCH;
/// Millennium Epoch: 1000 years
pub const MILLENNIUM_EPOCH: u128 = 1000 * 365 * DAILY_EPOCH;
/// Collider Epoch: 1 attosecond
pub const COLLIDER_EPOCH: u128 = 1;
/// Nano Epoch: 1 nanosecond
pub const NANO_EPOCH: u128 = 1_000_000_000;
// ============================================================================
// ANCHOR EPOCH
// ============================================================================
/// Anchor epoch type for timestamp interpretation
pub const AnchorEpoch = enum(u8) {
/// System boot (monotonic, default for local operations)
system_boot = 0,
/// Mission launch (for probes/long-term deployments)
mission_epoch = 1,
/// Unix epoch 1970-01-01T00:00:00Z (for interoperability)
unix_1970 = 2,
/// Bitcoin genesis block 2009-01-03T18:15:05Z (objective truth)
bitcoin_genesis = 3,
/// GPS epoch 1980-01-06T00:00:00Z (for precision timing)
gps_epoch = 4,
/// Bitcoin genesis in Unix seconds
pub const BITCOIN_GENESIS_UNIX: u64 = 1231006505;
/// GPS epoch in Unix seconds
pub const GPS_EPOCH_UNIX: u64 = 315964800;
/// Convert between epochs
pub fn toUnixOffset(self: AnchorEpoch) i128 {
return switch (self) {
.system_boot => 0, // Unknown offset
.mission_epoch => 0, // Mission-specific
.unix_1970 => 0,
.bitcoin_genesis => @as(i128, BITCOIN_GENESIS_UNIX) * @as(i128, ATTOSECONDS_PER_SECOND),
.gps_epoch => @as(i128, GPS_EPOCH_UNIX) * @as(i128, ATTOSECONDS_PER_SECOND),
};
}
};
// ============================================================================
// SOVEREIGN TIMESTAMP
// ============================================================================
/// Sovereign timestamp: u128 attoseconds since anchor epoch
/// Covers 10^21 years (beyond heat death of universe)
///
/// Wire format: 17 bytes (16 for u128 + 1 for anchor)
pub const SovereignTimestamp = struct {
/// Raw attoseconds value
raw: u128,
/// Anchor epoch type
anchor: AnchorEpoch,
pub const SERIALIZED_SIZE = 17;
/// Create from raw attoseconds
pub fn fromAttoseconds(as: u128, anchor: AnchorEpoch) SovereignTimestamp {
return .{ .raw = as, .anchor = anchor };
}
/// Create from nanoseconds (common hardware precision)
pub fn fromNanoseconds(ns: u64, anchor: AnchorEpoch) SovereignTimestamp {
return .{
.raw = @as(u128, ns) * ATTOSECONDS_PER_NANOSECOND,
.anchor = anchor,
};
}
/// Create from microseconds
pub fn fromMicroseconds(us: u64, anchor: AnchorEpoch) SovereignTimestamp {
return .{
.raw = @as(u128, us) * ATTOSECONDS_PER_MICROSECOND,
.anchor = anchor,
};
}
/// Create from milliseconds
pub fn fromMilliseconds(ms: u64, anchor: AnchorEpoch) SovereignTimestamp {
return .{
.raw = @as(u128, ms) * ATTOSECONDS_PER_MILLISECOND,
.anchor = anchor,
};
}
/// Create from seconds
pub fn fromSeconds(s: u64, anchor: AnchorEpoch) SovereignTimestamp {
return .{
.raw = @as(u128, s) * ATTOSECONDS_PER_SECOND,
.anchor = anchor,
};
}
/// Create from Unix timestamp (seconds since 1970)
pub fn fromUnixSeconds(unix_s: u64) SovereignTimestamp {
return fromSeconds(unix_s, .unix_1970);
}
/// Create from Unix timestamp (milliseconds since 1970)
pub fn fromUnixMillis(unix_ms: u64) SovereignTimestamp {
return fromMilliseconds(unix_ms, .unix_1970);
}
/// Get current time (platform-specific)
pub fn now() SovereignTimestamp {
// Use std.time for now, HAL will override
const ns = @as(u64, @intCast(std.time.nanoTimestamp()));
return fromNanoseconds(ns, .system_boot);
}
/// Convert to nanoseconds (may lose precision for very large values)
pub fn toNanoseconds(self: SovereignTimestamp) u128 {
return self.raw / ATTOSECONDS_PER_NANOSECOND;
}
/// Convert to microseconds
pub fn toMicroseconds(self: SovereignTimestamp) u128 {
return self.raw / ATTOSECONDS_PER_MICROSECOND;
}
/// Convert to milliseconds
pub fn toMilliseconds(self: SovereignTimestamp) u128 {
return self.raw / ATTOSECONDS_PER_MILLISECOND;
}
/// Convert to seconds
pub fn toSeconds(self: SovereignTimestamp) u128 {
return self.raw / ATTOSECONDS_PER_SECOND;
}
/// Convert to Unix timestamp (seconds since 1970)
/// Only valid if anchor is unix_1970 or bitcoin_genesis
pub fn toUnixSeconds(self: SovereignTimestamp) ?u64 {
const seconds = switch (self.anchor) {
.unix_1970 => self.raw / ATTOSECONDS_PER_SECOND,
.bitcoin_genesis => blk: {
const as_since_unix = self.raw + @as(u128, AnchorEpoch.BITCOIN_GENESIS_UNIX) * ATTOSECONDS_PER_SECOND;
break :blk as_since_unix / ATTOSECONDS_PER_SECOND;
},
else => return null,
};
if (seconds > std.math.maxInt(u64)) return null;
return @intCast(seconds);
}
/// Duration between two timestamps (signed)
pub fn diff(self: SovereignTimestamp, other: SovereignTimestamp) i128 {
// Handle the subtraction carefully to avoid overflow
if (self.raw >= other.raw) {
const delta = self.raw - other.raw;
// Cap at i128 max if too large
if (delta > @as(u128, std.math.maxInt(i128))) {
return std.math.maxInt(i128);
}
return @intCast(delta);
} else {
const delta = other.raw - self.raw;
// Cap at i128 min if too large
if (delta > @as(u128, std.math.maxInt(i128)) + 1) {
return std.math.minInt(i128);
}
return -@as(i128, @intCast(delta));
}
}
/// Duration since another timestamp (unsigned, assumes self > other)
pub fn since(self: SovereignTimestamp, other: SovereignTimestamp) u128 {
if (self.raw >= other.raw) {
return self.raw - other.raw;
}
return 0;
}
/// Check if this timestamp is after another
pub fn isAfter(self: SovereignTimestamp, other: SovereignTimestamp) bool {
return self.raw > other.raw;
}
/// Check if this timestamp is before another
pub fn isBefore(self: SovereignTimestamp, other: SovereignTimestamp) bool {
return self.raw < other.raw;
}
/// Add duration (attoseconds) - saturating
pub fn add(self: SovereignTimestamp, duration_as: u128) SovereignTimestamp {
return .{
.raw = self.raw +| duration_as, // Saturating add
.anchor = self.anchor,
};
}
/// Add seconds
pub fn addSeconds(self: SovereignTimestamp, seconds: u64) SovereignTimestamp {
return self.add(@as(u128, seconds) * ATTOSECONDS_PER_SECOND);
}
/// Subtract duration (attoseconds) - saturating
pub fn sub(self: SovereignTimestamp, duration_as: u128) SovereignTimestamp {
return .{
.raw = self.raw -| duration_as, // Saturating sub
.anchor = self.anchor,
};
}
/// Check if timestamp is within acceptable drift for vectors
pub fn isWithinDrift(self: SovereignTimestamp, reference: SovereignTimestamp, drift_tolerance: u128) bool {
const delta = if (self.raw >= reference.raw)
self.raw - reference.raw
else
reference.raw - self.raw;
return delta <= drift_tolerance;
}
/// Validate timestamp is not too far in future or too old
pub fn validateForVector(self: SovereignTimestamp, current: SovereignTimestamp) ValidationResult {
if (self.raw > current.raw + MAX_FUTURE_AS) {
return .too_far_future;
}
if (current.raw > self.raw + MAX_AGE_AS) {
return .too_old;
}
return .valid;
}
pub const ValidationResult = enum {
valid,
too_far_future,
too_old,
};
/// Serialize to wire format (17 bytes)
pub fn serialize(self: SovereignTimestamp) [SERIALIZED_SIZE]u8 {
var buf: [SERIALIZED_SIZE]u8 = undefined;
// u128 as two u64s (little-endian)
const low: u64 = @truncate(self.raw);
const high: u64 = @truncate(self.raw >> 64);
std.mem.writeInt(u64, buf[0..8], low, .little);
std.mem.writeInt(u64, buf[8..16], high, .little);
buf[16] = @intFromEnum(self.anchor);
return buf;
}
/// Deserialize from wire format
pub fn deserialize(data: *const [SERIALIZED_SIZE]u8) SovereignTimestamp {
const low = std.mem.readInt(u64, data[0..8], .little);
const high = std.mem.readInt(u64, data[8..16], .little);
const raw = (@as(u128, high) << 64) | @as(u128, low);
return .{
.raw = raw,
.anchor = @enumFromInt(data[16]),
};
}
};
// ============================================================================
// SOVEREIGN EPOCH
// ============================================================================
/// Standard Epoch Duration (1 Hour)
/// Used for Key Rotation, Session Renewal, and Cron synchronization.
pub const EPOCH_DURATION_AS: u128 = 3600 * ATTOSECONDS_PER_SECOND;
/// A Sovereign Epoch represents a fixed time slice in the timeline.
pub const Epoch = struct {
/// Sequential index of the epoch since Anchor
index: u64,
/// Get the epoch containing a specific timestamp
pub fn fromTimestamp(ts: SovereignTimestamp) Epoch {
// We calculate epoch relative to the generic timeline raw value
// Note: This implies different anchors might align epochs differently unless normalized.
// For simplicity, we define Epoch 0 starts at raw=0.
const idx = @as(u64, @intCast(ts.raw / EPOCH_DURATION_AS));
return .{ .index = idx };
}
/// Get current epoch
pub fn current() Epoch {
return fromTimestamp(SovereignTimestamp.now());
}
/// Get start timestamp of this epoch
pub fn startTime(self: Epoch, anchor: AnchorEpoch) SovereignTimestamp {
return SovereignTimestamp.fromAttoseconds(@as(u128, self.index) * EPOCH_DURATION_AS, anchor);
}
/// Get end timestamp of this epoch (exclusive)
pub fn endTime(self: Epoch, anchor: AnchorEpoch) SovereignTimestamp {
return SovereignTimestamp.fromAttoseconds(@as(u128, self.index + 1) * EPOCH_DURATION_AS, anchor);
}
/// Get duration until next epoch start (for sleep/cron)
pub fn timeRemaining(self: Epoch, current_ts: SovereignTimestamp) Duration {
const end_ts = self.endTime(current_ts.anchor);
return Duration.fromAttoseconds(end_ts.since(current_ts));
}
/// Check if a timestamp is within this epoch
pub fn contains(self: Epoch, ts: SovereignTimestamp) bool {
const other_idx = @as(u64, @intCast(ts.raw / EPOCH_DURATION_AS));
return self.index == other_idx;
}
/// Get next epoch
pub fn next(self: Epoch) Epoch {
return .{ .index = self.index + 1 };
}
/// Get previous epoch
pub fn prev(self: Epoch) Epoch {
if (self.index == 0) return self;
return .{ .index = self.index - 1 };
}
};
// ============================================================================
// COMPACT TIMESTAMP (Kenya Optimization)
// ============================================================================
/// Kenya-optimized timestamp storage (9 bytes vs 17)
/// Uses nanoseconds instead of attoseconds (good for ~584 years)
pub const CompactTimestamp = packed struct {
/// Nanoseconds since anchor
ns: u64,
/// Anchor epoch
anchor: AnchorEpoch,
pub const SERIALIZED_SIZE = 9;
/// Convert from SovereignTimestamp (loses sub-nanosecond precision)
pub fn fromSovereign(ts: SovereignTimestamp) CompactTimestamp {
const ns = ts.raw / ATTOSECONDS_PER_NANOSECOND;
return .{
.ns = if (ns > std.math.maxInt(u64)) std.math.maxInt(u64) else @intCast(ns),
.anchor = ts.anchor,
};
}
/// Convert to SovereignTimestamp
pub fn toSovereign(self: CompactTimestamp) SovereignTimestamp {
return SovereignTimestamp.fromNanoseconds(self.ns, self.anchor);
}
/// Serialize to wire format (9 bytes)
pub fn serialize(self: CompactTimestamp) [SERIALIZED_SIZE]u8 {
var buf: [SERIALIZED_SIZE]u8 = undefined;
std.mem.writeInt(u64, buf[0..8], self.ns, .little);
buf[8] = @intFromEnum(self.anchor);
return buf;
}
/// Deserialize from wire format
pub fn deserialize(data: *const [SERIALIZED_SIZE]u8) CompactTimestamp {
return .{
.ns = std.mem.readInt(u64, data[0..8], .little),
.anchor = @enumFromInt(data[8]),
};
}
};
// ============================================================================
// DURATION TYPE
// ============================================================================
/// Duration in attoseconds (for intervals, timeouts)
pub const Duration = struct {
as: u128,
pub fn fromAttoseconds(as: u128) Duration {
return .{ .as = as };
}
pub fn fromNanoseconds(ns: u64) Duration {
return .{ .as = @as(u128, ns) * ATTOSECONDS_PER_NANOSECOND };
}
pub fn fromMicroseconds(us: u64) Duration {
return .{ .as = @as(u128, us) * ATTOSECONDS_PER_MICROSECOND };
}
pub fn fromMilliseconds(ms: u64) Duration {
return .{ .as = @as(u128, ms) * ATTOSECONDS_PER_MILLISECOND };
}
pub fn fromSeconds(s: u64) Duration {
return .{ .as = @as(u128, s) * ATTOSECONDS_PER_SECOND };
}
pub fn fromMinutes(m: u64) Duration {
return fromSeconds(m * 60);
}
pub fn fromHours(h: u64) Duration {
return fromSeconds(h * 3600);
}
pub fn fromDays(d: u64) Duration {
return fromSeconds(d * 86400);
}
pub fn fromYears(y: u64) Duration {
// Gregorian average: 365.2425 days
return fromSeconds(y * 31556952);
}
/// 1 million years (probe hibernation test)
pub fn oneMillionYears() Duration {
return fromYears(1_000_000);
}
pub fn toNanoseconds(self: Duration) u128 {
return self.as / ATTOSECONDS_PER_NANOSECOND;
}
pub fn toSeconds(self: Duration) u128 {
return self.as / ATTOSECONDS_PER_SECOND;
}
};
// ============================================================================
// TESTS
// ============================================================================
test "SovereignTimestamp: basic creation" {
const ts = SovereignTimestamp.fromSeconds(1000, .unix_1970);
try std.testing.expectEqual(@as(u128, 1000) * ATTOSECONDS_PER_SECOND, ts.raw);
try std.testing.expectEqual(AnchorEpoch.unix_1970, ts.anchor);
}
test "SovereignTimestamp: unit conversions" {
const ts = SovereignTimestamp.fromSeconds(60, .unix_1970);
try std.testing.expectEqual(@as(u128, 60), ts.toSeconds());
try std.testing.expectEqual(@as(u128, 60_000), ts.toMilliseconds());
try std.testing.expectEqual(@as(u128, 60_000_000), ts.toMicroseconds());
try std.testing.expectEqual(@as(u128, 60_000_000_000), ts.toNanoseconds());
}
test "SovereignTimestamp: comparison" {
const ts1 = SovereignTimestamp.fromSeconds(100, .unix_1970);
const ts2 = SovereignTimestamp.fromSeconds(200, .unix_1970);
try std.testing.expect(ts2.isAfter(ts1));
try std.testing.expect(ts1.isBefore(ts2));
try std.testing.expectEqual(@as(i128, -100) * @as(i128, ATTOSECONDS_PER_SECOND), ts1.diff(ts2));
}
test "SovereignTimestamp: arithmetic" {
const ts1 = SovereignTimestamp.fromSeconds(100, .unix_1970);
const ts2 = ts1.addSeconds(50);
try std.testing.expectEqual(@as(u128, 150), ts2.toSeconds());
const ts3 = ts2.sub(25 * ATTOSECONDS_PER_SECOND);
try std.testing.expectEqual(@as(u128, 125), ts3.toSeconds());
}
test "SovereignTimestamp: serialization roundtrip" {
const original = SovereignTimestamp.fromSeconds(1706652000, .bitcoin_genesis);
const serialized = original.serialize();
const deserialized = SovereignTimestamp.deserialize(&serialized);
try std.testing.expectEqual(original.raw, deserialized.raw);
try std.testing.expectEqual(original.anchor, deserialized.anchor);
}
test "SovereignTimestamp: unix conversion" {
const ts = SovereignTimestamp.fromUnixSeconds(1706652000);
const unix = ts.toUnixSeconds();
try std.testing.expect(unix != null);
try std.testing.expectEqual(@as(u64, 1706652000), unix.?);
}
test "CompactTimestamp: conversion roundtrip" {
const original = SovereignTimestamp.fromSeconds(1000, .unix_1970);
const compact = CompactTimestamp.fromSovereign(original);
const restored = compact.toSovereign();
// Should match at nanosecond precision
try std.testing.expectEqual(original.toNanoseconds(), restored.toNanoseconds());
}
test "CompactTimestamp: serialization roundtrip" {
const original = CompactTimestamp{
.ns = 1706652000_000_000_000,
.anchor = .unix_1970,
};
const serialized = original.serialize();
const deserialized = CompactTimestamp.deserialize(&serialized);
try std.testing.expectEqual(original.ns, deserialized.ns);
try std.testing.expectEqual(original.anchor, deserialized.anchor);
}
test "Duration: one million years" {
const d = Duration.oneMillionYears();
// Verify it fits in u128
try std.testing.expect(d.as > 0);
// ~3.15576e31 attoseconds
const expected_as: u128 = 1_000_000 * 31556952 * ATTOSECONDS_PER_SECOND;
try std.testing.expectEqual(expected_as, d.as);
// Verify u128 has plenty of headroom
const u128_max: u128 = std.math.maxInt(u128);
try std.testing.expect(d.as < u128_max / 1_000_000); // Could store 1e6 more!
}
test "SovereignTimestamp: validation" {
const now = SovereignTimestamp.fromSeconds(1706652000, .unix_1970);
// Valid: within bounds
const valid = SovereignTimestamp.fromSeconds(1706651000, .unix_1970);
try std.testing.expectEqual(SovereignTimestamp.ValidationResult.valid, valid.validateForVector(now));
// Too far in future (> 1 hour)
const future = now.addSeconds(7200);
try std.testing.expectEqual(SovereignTimestamp.ValidationResult.too_far_future, future.validateForVector(now));
// Too old (> 30 days)
const old = now.sub(31 * 24 * 3600 * ATTOSECONDS_PER_SECOND);
try std.testing.expectEqual(SovereignTimestamp.ValidationResult.too_old, old.validateForVector(now));
}
test "Epoch: calculation" {
// 1 Hour = 3600 seconds
const t0 = SovereignTimestamp.fromSeconds(0, .unix_1970);
const e0 = Epoch.fromTimestamp(t0);
try std.testing.expectEqual(@as(u64, 0), e0.index);
const t1 = SovereignTimestamp.fromSeconds(3599, .unix_1970);
const e1 = Epoch.fromTimestamp(t1);
try std.testing.expectEqual(@as(u64, 0), e1.index);
const t2 = SovereignTimestamp.fromSeconds(3600, .unix_1970);
const e2 = Epoch.fromTimestamp(t2);
try std.testing.expectEqual(@as(u64, 1), e2.index);
// Remaining time
const rem = e0.timeRemaining(t1);
try std.testing.expectEqual(@as(u128, 1) * ATTOSECONDS_PER_SECOND, rem.as);
}