Skip to main content

phoxal_runtime_contract/
origin.rs

1//! Where one real execution started: a host boot, an offset on that boot's
2//! clock, and the timeline the execution runs on.
3//!
4//! The boot identity is what makes a boot-clock reading comparable at all. Two
5//! readings taken during different boots measure from different zeroes, so a
6//! consumer compares [`BootId`] first and only then the nanosecond offset.
7
8use std::time::Duration;
9
10use crate::identity::TimelineId;
11
12/// An identity for one host boot.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct BootId(u64);
15
16impl BootId {
17    /// This host's current boot identity.
18    pub fn current() -> Self {
19        Self(host_boot_identity())
20    }
21
22    /// Construct a boot identity from an opaque value.
23    ///
24    /// The value carries no structure, so this is only meaningful for a
25    /// consumer that already holds one: a decoded launch record, or a test
26    /// constructing a boot deliberately unequal to [`BootId::current`].
27    pub const fn from_raw(value: u64) -> Self {
28        Self(value)
29    }
30
31    /// The opaque launch-ABI representation.
32    pub const fn get(self) -> u64 {
33        self.0
34    }
35}
36
37/// The supervisor-minted origin of one real execution.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub struct ExecutionOrigin {
40    boot: BootId,
41    boot_ns: u64,
42    timeline: TimelineId,
43}
44
45impl ExecutionOrigin {
46    /// Mint an origin now, returning `None` when the host boot clock is unreadable.
47    pub fn try_mint() -> Option<Self> {
48        Some(Self {
49            boot: BootId::current(),
50            boot_ns: read_boot_clock_ns()?,
51            timeline: TimelineId::mint(),
52        })
53    }
54
55    /// Mint an origin now, panicking on a host without a readable boot clock.
56    ///
57    /// A supervisor that wants to report the failure rather than abort calls
58    /// [`ExecutionOrigin::try_mint`] instead. This form exists for the
59    /// bootstrap path, where a host whose monotonic clock cannot be read cannot
60    /// time an execution at all and there is nothing to degrade to.
61    #[expect(
62        clippy::expect_used,
63        reason = "every robot instant this execution stamps is an offset on the boot clock \
64                  read here; a host that cannot read it can produce no trustworthy instant, \
65                  so continuing would mean fabricating time"
66    )]
67    pub fn mint() -> Self {
68        Self::try_mint().expect("the host boot clock must be readable to start an execution")
69    }
70
71    /// Rebuild an origin from its typed fields.
72    ///
73    /// The counterpart to the accessors below, for a consumer that already
74    /// holds the three values: a decoded launch record, or a test pinning an
75    /// origin to a specific boot.
76    pub const fn new(boot: BootId, boot_ns: u64, timeline: TimelineId) -> Self {
77        Self {
78            boot,
79            boot_ns,
80            timeline,
81        }
82    }
83
84    /// The boot this origin was minted during.
85    pub const fn boot(self) -> BootId {
86        self.boot
87    }
88
89    /// Nanoseconds on the boot clock at execution start.
90    pub const fn boot_ns(self) -> u64 {
91        self.boot_ns
92    }
93
94    /// The real execution's timeline.
95    pub const fn timeline(self) -> TimelineId {
96        self.timeline
97    }
98
99    /// Render as `<boot>:<boot-ns>:<timeline>`.
100    pub fn encode(self) -> String {
101        format!("{}:{}:{}", self.boot.0, self.boot_ns, self.timeline.get())
102    }
103
104    /// Parse the launch representation without compatibility fallbacks.
105    pub fn decode(value: &str) -> Option<Self> {
106        let mut parts = value.split(':');
107        let boot = BootId(parts.next()?.parse().ok()?);
108        let boot_ns = parts.next()?.parse().ok()?;
109        let timeline = TimelineId::from_raw(parts.next()?.parse().ok()?)?;
110        if parts.next().is_some() {
111            return None;
112        }
113        Some(Self {
114            boot,
115            boot_ns,
116            timeline,
117        })
118    }
119}
120
121fn read_boot_clock_ns() -> Option<u64> {
122    // Keep this clock selection and nanosecond conversion identical to
123    // `phoxal_bus::LocalInstant`. This crate cannot depend on phoxal-bus
124    // without reversing the workspace ownership direction; the phoxal
125    // facade's clock tests compare both implementations directly.
126    #[cfg(target_os = "linux")]
127    const CLOCK: libc::clockid_t = libc::CLOCK_BOOTTIME;
128    #[cfg(not(target_os = "linux"))]
129    const CLOCK: libc::clockid_t = libc::CLOCK_MONOTONIC;
130
131    let mut timespec = libc::timespec {
132        tv_sec: 0,
133        tv_nsec: 0,
134    };
135    // SAFETY: `clock_gettime` writes into the owned `timespec`.
136    let outcome = unsafe { libc::clock_gettime(CLOCK, &raw mut timespec) };
137    if outcome != 0 {
138        return None;
139    }
140    Some(
141        u64::try_from(timespec.tv_sec)
142            .ok()?
143            .saturating_mul(1_000_000_000)
144            .saturating_add(u64::try_from(timespec.tv_nsec).ok()?),
145    )
146}
147
148fn host_boot_identity() -> u64 {
149    #[cfg(target_os = "linux")]
150    {
151        if let Ok(boot_id) = std::fs::read_to_string("/proc/sys/kernel/random/boot_id") {
152            return fnv1a(boot_id.trim().as_bytes());
153        }
154    }
155    #[cfg(target_os = "macos")]
156    {
157        let mut boottime = libc::timeval {
158            tv_sec: 0,
159            tv_usec: 0,
160        };
161        let mut size = std::mem::size_of::<libc::timeval>();
162        // SAFETY: `sysctlbyname` writes at most `size` bytes into `boottime`.
163        let outcome = unsafe {
164            libc::sysctlbyname(
165                c"kern.boottime".as_ptr(),
166                (&raw mut boottime).cast(),
167                &raw mut size,
168                std::ptr::null_mut(),
169                0,
170            )
171        };
172        if outcome == 0 {
173            return fnv1a(&boottime.tv_sec.to_le_bytes());
174        }
175    }
176    let wall = std::time::SystemTime::now()
177        .duration_since(std::time::UNIX_EPOCH)
178        .map(|since| since.as_secs())
179        .unwrap_or(0);
180    let uptime = read_boot_clock_ns()
181        .map(|ns| Duration::from_nanos(ns).as_secs())
182        .unwrap_or(0);
183    fnv1a(&wall.saturating_sub(uptime).to_le_bytes())
184}
185
186fn fnv1a(bytes: &[u8]) -> u64 {
187    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
188    for byte in bytes {
189        hash ^= u64::from(*byte);
190        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
191    }
192    hash
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn origin_round_trips_and_rejects_noncanonical_shapes() {
201        let origin = ExecutionOrigin::mint();
202        assert_eq!(ExecutionOrigin::decode(&origin.encode()), Some(origin));
203        assert_eq!(ExecutionOrigin::decode("garbage"), None);
204        assert_eq!(ExecutionOrigin::decode("1:2:0"), None);
205        assert_eq!(ExecutionOrigin::decode("1:2:3:4"), None);
206    }
207
208    #[test]
209    fn boot_identity_is_stable_within_one_boot() {
210        assert_eq!(BootId::current(), BootId::current());
211    }
212}