refactor(core): WallClockStore trait replaces the store_mut backdoor

Task 10b reached LogStore::set_wall_clock through a general Node::store_mut.
Kyle: replace it with a narrower accessor. A WallClockStore trait (set_wall_clock
+ peer_time_estimate, implemented only by LogStore) now backs Node::set_wall_clock
and Node::peer_time_estimate, so no &mut Store escapes the facade.
This commit is contained in:
2026-10-22 08:44:09 -07:00
parent a43a4966e1
commit ef539b494f
7 changed files with 58 additions and 17 deletions
+1 -1
View File
@@ -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};
+17 -8
View File
@@ -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<S: Store, T: Transport> Node<S, T> {
&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) -> <S as WallClockStore>::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<u64>
where
S: WallClockStore,
{
self.store.peer_time_estimate(now)
}
/// Best-effort durability flush, advancing each stored observation's age
+22
View File
@@ -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<u64>;
}
+2 -2
View File
@@ -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)"),
+13 -1
View File
@@ -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<u64> {
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)?;
+2 -2
View File
@@ -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)");
}
+1 -3
View File
@@ -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"