diff --git a/tines-core/src/lib.rs b/tines-core/src/lib.rs index 61587602..4d059052 100644 --- a/tines-core/src/lib.rs +++ b/tines-core/src/lib.rs @@ -25,7 +25,7 @@ pub use node::{Node, ObserveError}; pub use observation::{ Dest, NodeId, Observation, ObservationId, Payload, SigAlgo, Signature, Tick, Topic, }; -pub use store::{Store, StoreError, TrimPolicy}; +pub use store::{Store, StoreError, TrimPolicy, WallClockStore}; pub use transport::{Event, FrameBuf, SendError, Transport}; pub use wire::{frame_sender, Frame, WireError}; diff --git a/tines-core/src/node.rs b/tines-core/src/node.rs index 9e924fa4..3187e729 100644 --- a/tines-core/src/node.rs +++ b/tines-core/src/node.rs @@ -12,7 +12,7 @@ use crate::{ consts, exchange::Exchange, BeliefPolicy, Dest, NodeEvent, NodeId, Observation, ObservationId, - Payload, Store, StoreError, Tick, Topic, Transport, TrimPolicy, + Payload, Store, StoreError, Tick, Topic, Transport, TrimPolicy, WallClockStore, }; /// Errors returned by [`Node::observe`]. @@ -219,13 +219,22 @@ impl Node { &self.store } - /// Mutable access to the backing store. - /// - /// Used by the host for the trusted-time state machine — peer-observation - /// time estimation (`LogStore::peer_time_estimate`) and, once trusted, - /// anchoring the wall clock (`LogStore::set_wall_clock`). - pub fn store_mut(&mut self) -> &mut S { - &mut self.store + /// Lift the quarantine on held observations once the host has trusted wall + /// time, by folding the wall clock into a [`WallClockStore`] backend. + pub fn set_wall_clock(&mut self, wall_now: u64, now: Tick) -> ::FoldReport + where + S: WallClockStore, + { + self.store.set_wall_clock(wall_now, now) + } + + /// Best estimate of trusted wall time from a peer's observation, if the + /// backend supports it (see [`WallClockStore`]). + pub fn peer_time_estimate(&self, now: Tick) -> Option + where + S: WallClockStore, + { + self.store.peer_time_estimate(now) } /// Best-effort durability flush, advancing each stored observation's age diff --git a/tines-core/src/store.rs b/tines-core/src/store.rs index 6ea9e91e..2315dfe5 100644 --- a/tines-core/src/store.rs +++ b/tines-core/src/store.rs @@ -127,3 +127,25 @@ pub trait Store { pub(crate) fn current_age(obs: &Observation, stored_at: Tick, now: Tick) -> u64 { obs.age_ms.saturating_add(now.saturating_sub(stored_at)) } + +/// A [`Store`] that a host can feed trusted wall-clock time into. +/// +/// tines-core itself never deals with wall time — this trait is a host-facing +/// hook so a host can lift the quarantine on observations held across a +/// power-off, and estimate trusted time from a peer's observation, without a +/// general `&mut Store` backdoor (decision F in the spec). Only backends that +/// support a wall-clock fold implement it; `MemStore` and other pure-tick +/// stores do not. +pub trait WallClockStore: Store { + /// Summary of a wall-clock fold, for the host's logs. + type FoldReport; + + /// Fold trusted wall time into the store, lifting the quarantine on held + /// observations. The host calls this exactly once, on the first trusted + /// time. + fn set_wall_clock(&mut self, wall_now: u64, now: Tick) -> Self::FoldReport; + + /// Best estimate of trusted wall time from a peer's observation stored + /// since the last open, if any. `None` means stay quarantined. + fn peer_time_estimate(&self, now: Tick) -> Option; +} diff --git a/tines-ros2/src/node.rs b/tines-ros2/src/node.rs index 0031dc06..925645fa 100644 --- a/tines-ros2/src/node.rs +++ b/tines-ros2/src/node.rs @@ -223,10 +223,10 @@ impl TinesNode { if self.clock.is_trusted() { return; } - let Some(est) = self.tines.store_mut().peer_time_estimate(now) else { + let Some(est) = self.tines.peer_time_estimate(now) else { return; }; - let report = self.tines.store_mut().set_wall_clock(est, now); + let report = self.tines.set_wall_clock(est, now); self.clock.anchor_wall(est, now); match report.downtime_ms { Some(ms) => println!("tines: trusted wall clock from peer (folded {ms} ms downtime)"), diff --git a/tines-store/src/log.rs b/tines-store/src/log.rs index 761898ca..ea35a8f5 100644 --- a/tines-store/src/log.rs +++ b/tines-store/src/log.rs @@ -88,7 +88,7 @@ use std::io::Write; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; -use tines_core::{Observation, ObservationId, Store, StoreError, Tick, TrimPolicy}; +use tines_core::{Observation, ObservationId, Store, StoreError, Tick, TrimPolicy, WallClockStore}; mod crc32c { //! A minimal, std-only CRC-32C (Castagnoli). @@ -802,6 +802,18 @@ impl LogStore { } } +impl WallClockStore for LogStore { + type FoldReport = FoldReport; + + fn set_wall_clock(&mut self, wall_now: u64, now: Tick) -> FoldReport { + LogStore::set_wall_clock(self, wall_now, now) + } + + fn peer_time_estimate(&self, now: Tick) -> Option { + LogStore::peer_time_estimate(self, now) + } +} + /// Write the log header: `magic ‖ format_version ‖ reserved`. fn write_header(file: &mut File) -> Result<(), StoreError> { file.write_all(&MAGIC).map_err(|_| StoreError::Backend)?; diff --git a/tines/src/ipc.rs b/tines/src/ipc.rs index cb2a103b..f2a2fdd0 100644 --- a/tines/src/ipc.rs +++ b/tines/src/ipc.rs @@ -238,8 +238,8 @@ where // quarantined — the robot's own fail-safe is outside tines. if !clock.is_trusted() { let now = clock.now(); - if let Some(est) = node.store_mut().peer_time_estimate(now) { - node.store_mut().set_wall_clock(est, now); + if let Some(est) = node.peer_time_estimate(now) { + node.set_wall_clock(est, now); clock.anchor_wall(est, now); eprintln!("tines: trusted wall clock from peer (estimate {est} ms)"); } diff --git a/tines/tests/ipc.rs b/tines/tests/ipc.rs index 68f1626e..0567ce96 100644 --- a/tines/tests/ipc.rs +++ b/tines/tests/ipc.rs @@ -691,7 +691,6 @@ fn smallest_age_wins() { let now = t; let est = node_b - .store_mut() .peer_time_estimate(now) .expect("peer_time_estimate should find both observations"); // The young observation's observed_at (wall + 5000) is newer, so the @@ -744,7 +743,6 @@ fn time_from_peer() { // which is A's broadcast. let now_b = t; let est = node_b - .store_mut() .peer_time_estimate(now_b) .expect("peer_time_estimate should find A's broadcast"); @@ -763,7 +761,7 @@ fn time_from_peer() { // (unknown) downtime B is in, the fold ages its held observation past // lifetime and lifts its quarantine (the "becomes visible" event): the fold // report counts exactly that one addressed observation expiring. - let report = node_b.store_mut().set_wall_clock(est, now_b); + let report = node_b.set_wall_clock(est, now_b); assert_eq!( report.downtime_ms, None, "unknown downtime for a fresh store"