feat(l0): LWF v1.1 - 72-byte header with 24-byte DID hints

BREAKING CHANGE: Header size increased from 64 to 72 bytes

- Expand DID hints from 20 to 24 bytes (192-bit, 2^96 collision resistance)
- Clarify timestamp as u64 nanoseconds (Bytes 60-67, big-endian)
- Update frame payload capacities (-8 bytes per frame class)
- All tests passing (14/14 L0 tests)

Rationale:
- 24-byte DID hints provide future-proof routing scalability
- 8-byte overhead per frame is negligible (0.6% loss on Standard frames)
- Aligns with Sovereign Time Protocol (RFC-0105) L0/L1 split

Files modified:
- l0-transport/lwf.zig: Header structure, serialization, tests
- l0-transport/time.zig: New file for L0 time primitives
- build.zig: Time module dependencies

RFC Impact: RFC-0000 (LWF Wire Protocol), RFC-0105 (Sovereign Time)
This commit is contained in:
Markus Maiwald 2026-01-30 22:28:22 +01:00
parent ab84c1afbc
commit 76b05c7f49
3 changed files with 634 additions and 99 deletions

View File

@ -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

View File

@ -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());
}

491
l0-transport/time.zig Normal file
View File

@ -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));
}