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