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:
parent
ab84c1afbc
commit
76b05c7f49
48
build.zig
48
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
|
||||
|
|
|
|||
|
|
@ -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) │
|
||||
//! ├──────────────────┤
|
||||
|
|
@ -49,21 +49,21 @@ pub const LWFFlags = struct {
|
|||
pub const PRIORITY: u8 = 0x20; // High-priority frame
|
||||
};
|
||||
|
||||
/// RFC-0000 Section 4.2: LWF Header (64 bytes fixed)
|
||||
pub const LWFHeader = extern struct {
|
||||
/// 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: [20]u8, // Blake3 truncated DID hint
|
||||
dest_hint: [20]u8, // Blake3 truncated DID hint
|
||||
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, Unix epoch milliseconds
|
||||
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;
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
Loading…
Reference in New Issue