Skip to main content

phoxal_runtime_contract/
origin.rs

1use std::time::Duration;
2
3use crate::TimelineId;
4
5/// An identity for one host boot.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub struct BootId(u64);
8
9impl BootId {
10    /// This host's current boot identity.
11    pub fn current() -> Self {
12        Self(host_boot_identity())
13    }
14
15    /// Construct a boot identity for contract tests.
16    #[doc(hidden)]
17    pub const fn from_raw(value: u64) -> Self {
18        Self(value)
19    }
20
21    /// The opaque launch-ABI representation.
22    pub const fn get(self) -> u64 {
23        self.0
24    }
25}
26
27/// The supervisor-minted origin of one real execution.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub struct ExecutionOrigin {
30    boot: BootId,
31    boot_ns: u64,
32    timeline: TimelineId,
33}
34
35impl ExecutionOrigin {
36    /// Mint an origin now, returning `None` when the host boot clock is unreadable.
37    pub fn try_mint() -> Option<Self> {
38        Some(Self {
39            boot: BootId::current(),
40            boot_ns: read_boot_clock_ns()?,
41            timeline: TimelineId::mint(),
42        })
43    }
44
45    /// Mint an origin now, panicking on a host without a readable boot clock.
46    pub fn mint() -> Self {
47        Self::try_mint().expect("the host boot clock must be readable to start an execution")
48    }
49
50    /// Rebuild an origin from its typed fields.
51    #[doc(hidden)]
52    pub const fn new(boot: BootId, boot_ns: u64, timeline: TimelineId) -> Self {
53        Self {
54            boot,
55            boot_ns,
56            timeline,
57        }
58    }
59
60    /// The boot this origin was minted during.
61    pub const fn boot(self) -> BootId {
62        self.boot
63    }
64
65    /// Nanoseconds on the boot clock at execution start.
66    pub const fn boot_ns(self) -> u64 {
67        self.boot_ns
68    }
69
70    /// The real execution's timeline.
71    pub const fn timeline(self) -> TimelineId {
72        self.timeline
73    }
74
75    /// Render as `<boot>:<boot-ns>:<timeline>`.
76    pub fn encode(self) -> String {
77        format!("{}:{}:{}", self.boot.0, self.boot_ns, self.timeline.get())
78    }
79
80    /// Parse the launch representation without compatibility fallbacks.
81    pub fn decode(value: &str) -> Option<Self> {
82        let mut parts = value.split(':');
83        let boot = BootId(parts.next()?.parse().ok()?);
84        let boot_ns = parts.next()?.parse().ok()?;
85        let timeline = TimelineId::from_raw(parts.next()?.parse().ok()?)?;
86        if parts.next().is_some() {
87            return None;
88        }
89        Some(Self {
90            boot,
91            boot_ns,
92            timeline,
93        })
94    }
95}
96
97fn read_boot_clock_ns() -> Option<u64> {
98    // Keep this clock selection and nanosecond conversion identical to
99    // `phoxal_bus::LocalInstant`. This crate cannot depend on phoxal-bus
100    // without reversing the workspace ownership direction; the phoxal
101    // facade's clock tests compare both implementations directly.
102    #[cfg(target_os = "linux")]
103    const CLOCK: libc::clockid_t = libc::CLOCK_BOOTTIME;
104    #[cfg(not(target_os = "linux"))]
105    const CLOCK: libc::clockid_t = libc::CLOCK_MONOTONIC;
106
107    let mut timespec = libc::timespec {
108        tv_sec: 0,
109        tv_nsec: 0,
110    };
111    // SAFETY: `clock_gettime` writes into the owned `timespec`.
112    let outcome = unsafe { libc::clock_gettime(CLOCK, &raw mut timespec) };
113    if outcome != 0 {
114        return None;
115    }
116    Some(
117        u64::try_from(timespec.tv_sec)
118            .ok()?
119            .saturating_mul(1_000_000_000)
120            .saturating_add(u64::try_from(timespec.tv_nsec).ok()?),
121    )
122}
123
124fn host_boot_identity() -> u64 {
125    #[cfg(target_os = "linux")]
126    {
127        if let Ok(boot_id) = std::fs::read_to_string("/proc/sys/kernel/random/boot_id") {
128            return fnv1a(boot_id.trim().as_bytes());
129        }
130    }
131    #[cfg(target_os = "macos")]
132    {
133        let mut boottime = libc::timeval {
134            tv_sec: 0,
135            tv_usec: 0,
136        };
137        let mut size = std::mem::size_of::<libc::timeval>();
138        // SAFETY: `sysctlbyname` writes at most `size` bytes into `boottime`.
139        let outcome = unsafe {
140            libc::sysctlbyname(
141                c"kern.boottime".as_ptr(),
142                (&raw mut boottime).cast(),
143                &raw mut size,
144                std::ptr::null_mut(),
145                0,
146            )
147        };
148        if outcome == 0 {
149            return fnv1a(&boottime.tv_sec.to_le_bytes());
150        }
151    }
152    let wall = std::time::SystemTime::now()
153        .duration_since(std::time::UNIX_EPOCH)
154        .map(|since| since.as_secs())
155        .unwrap_or(0);
156    let uptime = read_boot_clock_ns()
157        .map(|ns| Duration::from_nanos(ns).as_secs())
158        .unwrap_or(0);
159    fnv1a(&wall.saturating_sub(uptime).to_le_bytes())
160}
161
162fn fnv1a(bytes: &[u8]) -> u64 {
163    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
164    for byte in bytes {
165        hash ^= u64::from(*byte);
166        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
167    }
168    hash
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn origin_round_trips_and_rejects_noncanonical_shapes() {
177        let origin = ExecutionOrigin::mint();
178        assert_eq!(ExecutionOrigin::decode(&origin.encode()), Some(origin));
179        assert_eq!(ExecutionOrigin::decode("garbage"), None);
180        assert_eq!(ExecutionOrigin::decode("1:2:0"), None);
181        assert_eq!(ExecutionOrigin::decode("1:2:3:4"), None);
182    }
183
184    #[test]
185    fn boot_identity_is_stable_within_one_boot() {
186        assert_eq!(BootId::current(), BootId::current());
187    }
188}