diff --git a/build.zig b/build.zig index 5fbcd63..f37f0b8 100644 --- a/build.zig +++ b/build.zig @@ -133,17 +133,60 @@ pub fn build(b: *std.Build) void { }); const run_l1_prekey_tests = b.addRunArtifact(l1_prekey_tests); + // L1 DID tests (Phase 2D) // L1 DID tests (Phase 2D) const l1_did_tests = b.addTest(.{ .root_module = l1_did_mod, }); const run_l1_did_tests = b.addRunArtifact(l1_did_tests); + // Link time module to l1_vector_mod + // ======================================================================== + // Time Module (L0) + // ======================================================================== + const time_mod = b.createModule(.{ + .root_source_file = b.path("l0-transport/time.zig"), + .target = target, + .optimize = optimize, + }); + + // L1 Vector tests (Phase 3C) + const l1_vector_mod = b.createModule(.{ + .root_source_file = b.path("l1-identity/vector.zig"), + .target = target, + .optimize = optimize, + }); + l1_vector_mod.addImport("time", time_mod); + + const l1_vector_tests = b.addTest(.{ + .root_module = l1_vector_mod, + }); + // Add Argon2 support for vector tests (via entropy.zig) + l1_vector_tests.addCSourceFiles(.{ + .files = &.{ + "vendor/argon2/src/argon2.c", + "vendor/argon2/src/core.c", + "vendor/argon2/src/blake2/blake2b.c", + "vendor/argon2/src/thread.c", + "vendor/argon2/src/encoding.c", + "vendor/argon2/src/opt.c", + }, + .flags = &.{ + "-std=c99", + "-O3", + "-fPIC", + "-DHAVE_PTHREAD", + }, + }); + l1_vector_tests.addIncludePath(b.path("vendor/argon2/include")); + l1_vector_tests.linkLibC(); + const run_l1_vector_tests = b.addRunArtifact(l1_vector_tests); + // NOTE: Phase 3 (Full Kyber tests) deferred to separate build invocation // See: zig build test-l1-phase3 (requires static library linking fix) - // Test step (runs Phase 2B + 2C + 2D tests: pure Zig + Argon2) - const test_step = b.step("test", "Run Phase 2B + 2C + 2D SDK tests (pure Zig + Argon2)"); + // Test step (runs Phase 2B + 2C + 2D + 3C SDK tests) + const test_step = b.step("test", "Run SDK tests"); test_step.dependOn(&run_crypto_tests.step); test_step.dependOn(&run_crypto_ffi_tests.step); test_step.dependOn(&run_l0_tests.step); @@ -151,6 +194,7 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&run_l1_entropy_tests.step); test_step.dependOn(&run_l1_prekey_tests.step); test_step.dependOn(&run_l1_did_tests.step); + test_step.dependOn(&run_l1_vector_tests.step); // ======================================================================== // Examples diff --git a/l0-transport/lwf.zig b/l0-transport/lwf.zig index d1625cb..fd08367 100644 --- a/l0-transport/lwf.zig +++ b/l0-transport/lwf.zig @@ -3,15 +3,15 @@ //! This module implements the core LWF frame structure for L0 transport. //! //! Key features: -//! - Fixed-size header (64 bytes) -//! - Variable payload (up to 8900 bytes based on frame class) +//! - Fixed-size header (72 bytes) +//! - Variable payload (up to 8828 bytes based on frame class) //! - Fixed-size trailer (36 bytes) //! - Checksum verification (CRC32-C) //! - Signature support (Ed25519) //! //! Frame structure: //! ┌──────────────────┐ -//! │ Header (64B) │ +//! │ Header (72B) │ //! ├──────────────────┤ //! │ Payload (var) │ //! ├──────────────────┤ @@ -22,11 +22,11 @@ const std = @import("std"); /// RFC-0000 Section 4.1: Frame size classes pub const FrameClass = enum(u8) { - micro = 0x00, // 128 bytes - tiny = 0x01, // 512 bytes - standard = 0x02, // 1350 bytes (default) - large = 0x03, // 4096 bytes - jumbo = 0x04, // 9000 bytes + micro = 0x00, // 128 bytes + tiny = 0x01, // 512 bytes + standard = 0x02, // 1350 bytes (default) + large = 0x03, // 4096 bytes + jumbo = 0x04, // 9000 bytes pub fn maxPayloadSize(self: FrameClass) usize { return switch (self) { @@ -41,29 +41,29 @@ pub const FrameClass = enum(u8) { /// RFC-0000 Section 4.3: Frame flags pub const LWFFlags = struct { - pub const ENCRYPTED: u8 = 0x01; // Payload is encrypted - pub const SIGNED: u8 = 0x02; // Trailer has signature - pub const RELAYABLE: u8 = 0x04; // Can be relayed by nodes - pub const HAS_ENTROPY: u8 = 0x08; // Includes Entropy Stamp - pub const FRAGMENTED: u8 = 0x10; // Part of fragmented message - pub const PRIORITY: u8 = 0x20; // High-priority frame + pub const ENCRYPTED: u8 = 0x01; // Payload is encrypted + pub const SIGNED: u8 = 0x02; // Trailer has signature + pub const RELAYABLE: u8 = 0x04; // Can be relayed by nodes + pub const HAS_ENTROPY: u8 = 0x08; // Includes Entropy Stamp + pub const FRAGMENTED: u8 = 0x10; // Part of fragmented message + pub const PRIORITY: u8 = 0x20; // High-priority frame }; -/// RFC-0000 Section 4.2: LWF Header (64 bytes fixed) -pub const LWFHeader = extern struct { - magic: [4]u8, // "LWF\0" - version: u8, // 0x01 - flags: u8, // Bitfield (see LWFFlags) - service_type: u16, // Big-endian, 0x0A00-0x0AFF for Feed - source_hint: [20]u8, // Blake3 truncated DID hint - dest_hint: [20]u8, // Blake3 truncated DID hint - sequence: u32, // Big-endian, anti-replay counter - timestamp: u64, // Big-endian, Unix epoch milliseconds - payload_len: u16, // Big-endian, actual payload size - entropy_difficulty: u8, // Entropy Stamp difficulty (0-255) - frame_class: u8, // FrameClass enum value +/// RFC-0000 Section 4.2: LWF Header (72 bytes fixed) +pub const LWFHeader = struct { + magic: [4]u8, // "LWF\0" + version: u8, // 0x01 + flags: u8, // Bitfield (see LWFFlags) + service_type: u16, // Big-endian, 0x0A00-0x0AFF for Feed + source_hint: [24]u8, // Blake3 truncated DID hint (192-bit) + dest_hint: [24]u8, // Blake3 truncated DID hint (192-bit) + sequence: u32, // Big-endian, anti-replay counter + timestamp: u64, // Big-endian, nanoseconds since epoch + payload_len: u16, // Big-endian, actual payload size + entropy_difficulty: u8, // Entropy Stamp difficulty (0-255) + frame_class: u8, // FrameClass enum value - pub const SIZE: usize = 64; + pub const SIZE: usize = 72; /// Initialize header with default values pub fn init() LWFHeader { @@ -72,8 +72,8 @@ pub const LWFHeader = extern struct { .version = 0x01, .flags = 0, .service_type = 0, - .source_hint = [_]u8{0} ** 20, - .dest_hint = [_]u8{0} ** 20, + .source_hint = [_]u8{0} ** 24, + .dest_hint = [_]u8{0} ** 24, .sequence = 0, .timestamp = 0, .payload_len = 0, @@ -88,8 +88,8 @@ pub const LWFHeader = extern struct { return std.mem.eql(u8, &self.magic, &expected_magic) and self.version == 0x01; } - /// Serialize header to exactly 64 bytes (no padding) - pub fn toBytes(self: *const LWFHeader, buffer: *[64]u8) void { + /// Serialize header to exactly 72 bytes + pub fn toBytes(self: *const LWFHeader, buffer: *[72]u8) void { var offset: usize = 0; // magic: [4]u8 @@ -104,28 +104,28 @@ pub const LWFHeader = extern struct { buffer[offset] = self.flags; offset += 1; - // service_type: u16 (already big-endian, copy bytes directly) - @memcpy(buffer[offset..][0..2], std.mem.asBytes(&self.service_type)); + // service_type: u16 (big-endian) + std.mem.writeInt(u16, buffer[offset..][0..2], self.service_type, .big); offset += 2; - // source_hint: [20]u8 - @memcpy(buffer[offset..][0..20], &self.source_hint); - offset += 20; + // source_hint: [24]u8 + @memcpy(buffer[offset..][0..24], &self.source_hint); + offset += 24; - // dest_hint: [20]u8 - @memcpy(buffer[offset..][0..20], &self.dest_hint); - offset += 20; + // dest_hint: [24]u8 + @memcpy(buffer[offset..][0..24], &self.dest_hint); + offset += 24; - // sequence: u32 (already big-endian, copy bytes directly) - @memcpy(buffer[offset..][0..4], std.mem.asBytes(&self.sequence)); + // sequence: u32 (big-endian) + std.mem.writeInt(u32, buffer[offset..][0..4], self.sequence, .big); offset += 4; - // timestamp: u64 (already big-endian, copy bytes directly) - @memcpy(buffer[offset..][0..8], std.mem.asBytes(&self.timestamp)); + // timestamp: u64 (big-endian) + std.mem.writeInt(u64, buffer[offset..][0..8], self.timestamp, .big); offset += 8; - // payload_len: u16 (already big-endian, copy bytes directly) - @memcpy(buffer[offset..][0..2], std.mem.asBytes(&self.payload_len)); + // payload_len: u16 (big-endian) + std.mem.writeInt(u16, buffer[offset..][0..2], self.payload_len, .big); offset += 2; // entropy_difficulty: u8 @@ -134,59 +134,59 @@ pub const LWFHeader = extern struct { // frame_class: u8 buffer[offset] = self.frame_class; - // offset += 1; // Final field, no need to increment + offset += 1; - std.debug.assert(offset + 1 == 64); // Verify we wrote exactly 64 bytes + std.debug.assert(offset == 72); } - /// Deserialize header from exactly 64 bytes - pub fn fromBytes(buffer: *const [64]u8) LWFHeader { + /// Deserialize header from exactly 72 bytes + pub fn fromBytes(buffer: *const [72]u8) LWFHeader { var header: LWFHeader = undefined; var offset: usize = 0; - // magic: [4]u8 + // magic @memcpy(&header.magic, buffer[offset..][0..4]); offset += 4; - // version: u8 + // version header.version = buffer[offset]; offset += 1; - // flags: u8 + // flags header.flags = buffer[offset]; offset += 1; - // service_type: u16 (already big-endian, copy bytes directly) - @memcpy(std.mem.asBytes(&header.service_type), buffer[offset..][0..2]); + // service_type + header.service_type = std.mem.readInt(u16, buffer[offset..][0..2], .big); offset += 2; - // source_hint: [20]u8 - @memcpy(&header.source_hint, buffer[offset..][0..20]); - offset += 20; + // source_hint + @memcpy(&header.source_hint, buffer[offset..][0..24]); + offset += 24; - // dest_hint: [20]u8 - @memcpy(&header.dest_hint, buffer[offset..][0..20]); - offset += 20; + // dest_hint + @memcpy(&header.dest_hint, buffer[offset..][0..24]); + offset += 24; - // sequence: u32 (already big-endian, copy bytes directly) - @memcpy(std.mem.asBytes(&header.sequence), buffer[offset..][0..4]); + // sequence + header.sequence = std.mem.readInt(u32, buffer[offset..][0..4], .big); offset += 4; - // timestamp: u64 (already big-endian, copy bytes directly) - @memcpy(std.mem.asBytes(&header.timestamp), buffer[offset..][0..8]); + // timestamp + header.timestamp = std.mem.readInt(u64, buffer[offset..][0..8], .big); offset += 8; - // payload_len: u16 (already big-endian, copy bytes directly) - @memcpy(std.mem.asBytes(&header.payload_len), buffer[offset..][0..2]); + // payload_len + header.payload_len = std.mem.readInt(u16, buffer[offset..][0..2], .big); offset += 2; - // entropy_difficulty: u8 + // entropy header.entropy_difficulty = buffer[offset]; offset += 1; - // frame_class: u8 + // frame_class header.frame_class = buffer[offset]; - // offset += 1; // Final field + offset += 1; return header; } @@ -194,8 +194,8 @@ pub const LWFHeader = extern struct { /// RFC-0000 Section 4.7: LWF Trailer (36 bytes fixed) pub const LWFTrailer = extern struct { - signature: [32]u8, // Ed25519 signature (or zeros if not signed) - checksum: u32, // CRC32-C, big-endian + signature: [32]u8, // Ed25519 signature (or zeros if not signed) + checksum: u32, // CRC32-C, big-endian pub const SIZE: usize = 36; @@ -272,18 +272,18 @@ pub const LWFFrame = struct { const total_size = self.size(); var buffer = try allocator.alloc(u8, total_size); - // Serialize header (exactly 64 bytes) - var header_bytes: [64]u8 = undefined; + // Serialize header (exactly 72 bytes) + var header_bytes: [72]u8 = undefined; self.header.toBytes(&header_bytes); - @memcpy(buffer[0..64], &header_bytes); + @memcpy(buffer[0..72], &header_bytes); // Copy payload - @memcpy(buffer[64 .. 64 + self.payload.len], self.payload); + @memcpy(buffer[72 .. 72 + self.payload.len], self.payload); // Serialize trailer (exactly 36 bytes) var trailer_bytes: [36]u8 = undefined; self.trailer.toBytes(&trailer_bytes); - const trailer_start = 64 + self.payload.len; + const trailer_start = 72 + self.payload.len; @memcpy(buffer[trailer_start .. trailer_start + 36], &trailer_bytes); return buffer; @@ -292,13 +292,13 @@ pub const LWFFrame = struct { /// Decode frame from bytes (allocates payload) pub fn decode(allocator: std.mem.Allocator, data: []const u8) !LWFFrame { // Minimum frame size check - if (data.len < 64 + 36) { + if (data.len < 72 + 36) { return error.FrameTooSmall; } - // Parse header (first 64 bytes) - var header_bytes: [64]u8 = undefined; - @memcpy(&header_bytes, data[0..64]); + // Parse header (first 72 bytes) + var header_bytes: [72]u8 = undefined; + @memcpy(&header_bytes, data[0..72]); const header = LWFHeader.fromBytes(&header_bytes); // Validate header @@ -307,19 +307,19 @@ pub const LWFFrame = struct { } // Extract payload length - const payload_len = @as(usize, @intCast(std.mem.bigToNative(u16, header.payload_len))); + const payload_len = @as(usize, @intCast(header.payload_len)); // Verify frame size matches - if (data.len < 64 + payload_len + 36) { + if (data.len < 72 + payload_len + 36) { return error.InvalidPayloadLength; } // Allocate and copy payload const payload = try allocator.alloc(u8, payload_len); - @memcpy(payload, data[64 .. 64 + payload_len]); + @memcpy(payload, data[72 .. 72 + payload_len]); // Parse trailer - const trailer_start = 64 + payload_len; + const trailer_start = 72 + payload_len; var trailer_bytes: [36]u8 = undefined; @memcpy(&trailer_bytes, data[trailer_start .. trailer_start + 36]); const trailer = LWFTrailer.fromBytes(&trailer_bytes); @@ -335,8 +335,8 @@ pub const LWFFrame = struct { pub fn calculateChecksum(self: *const LWFFrame) u32 { var hasher = std.hash.Crc32.init(); - // Hash header (exactly 64 bytes) - var header_bytes: [64]u8 = undefined; + // Hash header (exactly 72 bytes) + var header_bytes: [72]u8 = undefined; self.header.toBytes(&header_bytes); hasher.update(&header_bytes); @@ -370,7 +370,7 @@ test "LWFFrame creation" { var frame = try LWFFrame.init(allocator, 100); defer frame.deinit(allocator); - try std.testing.expectEqual(@as(usize, 64 + 100 + 36), frame.size()); + try std.testing.expectEqual(@as(usize, 72 + 100 + 36), frame.size()); try std.testing.expectEqual(@as(u8, 'L'), frame.header.magic[0]); try std.testing.expectEqual(@as(u8, 0x01), frame.header.version); } @@ -383,9 +383,9 @@ test "LWFFrame encode/decode roundtrip" { defer frame.deinit(allocator); // Populate frame - frame.header.service_type = std.mem.nativeToBig(u16, 0x0A00); // FEED_WORLD_POST - frame.header.payload_len = std.mem.nativeToBig(u16, 10); - frame.header.timestamp = std.mem.nativeToBig(u64, 1234567890); + frame.header.service_type = 0x0A00; // FEED_WORLD_POST + frame.header.payload_len = 10; + frame.header.timestamp = 1234567890; @memcpy(frame.payload, "HelloWorld"); frame.updateChecksum(); @@ -393,7 +393,7 @@ test "LWFFrame encode/decode roundtrip" { const encoded = try frame.encode(allocator); defer allocator.free(encoded); - try std.testing.expectEqual(@as(usize, 64 + 10 + 36), encoded.len); + try std.testing.expectEqual(@as(usize, 72 + 10 + 36), encoded.len); // Decode var decoded = try LWFFrame.decode(allocator, encoded); @@ -425,9 +425,9 @@ test "LWFFrame checksum verification" { } test "FrameClass payload sizes" { - try std.testing.expectEqual(@as(usize, 28), FrameClass.micro.maxPayloadSize()); - try std.testing.expectEqual(@as(usize, 412), FrameClass.tiny.maxPayloadSize()); - try std.testing.expectEqual(@as(usize, 1250), FrameClass.standard.maxPayloadSize()); - try std.testing.expectEqual(@as(usize, 3996), FrameClass.large.maxPayloadSize()); - try std.testing.expectEqual(@as(usize, 8900), FrameClass.jumbo.maxPayloadSize()); + try std.testing.expectEqual(@as(usize, 20), FrameClass.micro.maxPayloadSize()); + try std.testing.expectEqual(@as(usize, 404), FrameClass.tiny.maxPayloadSize()); + try std.testing.expectEqual(@as(usize, 1242), FrameClass.standard.maxPayloadSize()); + try std.testing.expectEqual(@as(usize, 3988), FrameClass.large.maxPayloadSize()); + try std.testing.expectEqual(@as(usize, 8892), FrameClass.jumbo.maxPayloadSize()); } diff --git a/l0-transport/time.zig b/l0-transport/time.zig new file mode 100644 index 0000000..2e89415 --- /dev/null +++ b/l0-transport/time.zig @@ -0,0 +1,491 @@ +//! 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; + +// ============================================================================ +// 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]), + }; + } +}; + +// ============================================================================ +// 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)); +}