fix(hosts): pass the plausible wall range to the store

Both hosts open LogStore with plausible_wall_ms(PLAUSIBLE_WALL_MS), so a
forged out-of-range peer estimate is dropped before the store picks the
latest and can't hide an honest one (decision J, review B3). Un-ignores
latest_estimate_wins_over_smallest_age now that the store returns the
latest estimate, and adds forged_estimate_does_not_hide_an_honest_one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 08:44:09 -07:00
co-authored by Claude Opus 5.5
parent 15bbdbd4a0
commit d91d7914fc
4 changed files with 40 additions and 6 deletions
+4 -1
View File
@@ -172,7 +172,10 @@ impl TinesNode {
let executor = context.create_basic_executor();
let node = executor.create_node("tines")?;
let store = LogStore::open(log_path, 0)?;
// Peer estimates outside the plausible range don't count (decision J).
let store = LogStore::builder(log_path)
.plausible_wall_ms(tines::trust::PLAUSIBLE_WALL_MS)
.open(0)?;
let transport = BtTransport::new(me, peer, radio);
let tines = TinesNodeCore::new(me, store.first_seq(), store, transport);
+5 -2
View File
@@ -124,9 +124,12 @@ fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
// LogStore is the Store (S); BtTransport wraps it as the Transport (T).
// unknown_downtime_age_ms (the broadcast floor while the wall clock is
// unknown) is optional; leave the LogStore default when unset.
// Peer estimates outside the plausible range don't count (decision J).
let builder = LogStore::builder(Path::new(&config.log_path))
.plausible_wall_ms(tines::trust::PLAUSIBLE_WALL_MS);
let builder = match config.unknown_downtime_age_ms {
Some(ms) => LogStore::builder(Path::new(&config.log_path)).set_unknown_downtime_age_ms(ms),
None => LogStore::builder(Path::new(&config.log_path)),
Some(ms) => builder.set_unknown_downtime_age_ms(ms),
None => builder,
};
let mut clock = HostClock::start();
let store = builder
+7 -1
View File
@@ -48,6 +48,12 @@ pub const WALL_FLOOR_MS: u64 = 1_767_225_600_000;
/// still far below where `u64` milliseconds overflow.
pub const WALL_CEILING_MS: u64 = WALL_FLOOR_MS + 36_525 * 24 * 60 * 60 * 1000;
/// [`WALL_FLOOR_MS`]`..=`[`WALL_CEILING_MS`] as a range, for
/// `LogStoreBuilder::plausible_wall_ms`. The store drops peer estimates outside
/// it before choosing the latest, so one forged estimate can't hide the honest
/// ones (decision J).
pub const PLAUSIBLE_WALL_MS: core::ops::RangeInclusive<u64> = WALL_FLOOR_MS..=WALL_CEILING_MS;
/// The default peer-time window: how long a host keeps collecting peer
/// estimates after the first plausible one before adopting the latest
/// (decision J, about 10 s).
@@ -56,7 +62,7 @@ pub const DEFAULT_PEER_TIME_WINDOW_MS: u64 = 10_000;
/// Whether `wall_ms` (ms since the Unix epoch) lies within
/// [`WALL_FLOOR_MS`]`..=`[`WALL_CEILING_MS`], so a host may adopt it.
pub fn is_plausible_wall_ms(wall_ms: u64) -> bool {
(WALL_FLOOR_MS..=WALL_CEILING_MS).contains(&wall_ms)
PLAUSIBLE_WALL_MS.contains(&wall_ms)
}
/// The system clock in ms since the Unix epoch, if it is plausible.
+24 -2
View File
@@ -478,7 +478,10 @@ fn build_pair(dir: &Path) -> Pair {
let world = Arc::new(Mutex::new(SharedInMemoryWorld::new()));
let (a_id, b_id) = ([1; 16], [2; 16]);
let a_store = LogStore::open(&dir.join("a.store.log"), 0).expect("a store open");
let b_store = LogStore::open(&dir.join("b.store.log"), 0).expect("b store open");
let b_store = LogStore::builder(&dir.join("b.store.log"))
.plausible_wall_ms(tines::trust::PLAUSIBLE_WALL_MS)
.open(0)
.expect("b store open");
let ta = SharedInMemory {
me: a_id,
their: b_id,
@@ -816,7 +819,6 @@ fn window_adopts_a_later_larger_estimate() {
/// `W + 6000`). Smallest-age selection would adopt `W`; the host adopts the
/// latest estimate.
#[test]
#[ignore = "needs decision J store change"]
fn latest_estimate_wins_over_smallest_age() {
let dir = unique_dir("latest");
let (mut node_a, node_b, world) = build_pair(&dir);
@@ -868,6 +870,26 @@ fn forged_max_observed_at_is_not_adopted() {
assert_not_adopted("forged", u64::MAX);
}
/// Decision J with B3's bounds: a forged `u64::MAX` alongside an honest
/// estimate doesn't hide it. The store drops the forged one before choosing
/// the latest, so B adopts the honest time.
#[test]
fn forged_estimate_does_not_hide_an_honest_one() {
let dir = unique_dir("forged-honest");
let (mut node_a, node_b, world) = build_pair(&dir);
let socket_b = serve_b(node_b, HostClock::start(), &dir);
inject(&world, &crafted(1, u64::MAX, 0));
inject(&world, &crafted(2, W, 0));
let mut t = 0;
let at = b_adopted_stamp(&socket_b, &mut node_a, &mut t);
assert!(
(W..W + 60_000).contains(&at),
"B adopted the honest estimate: stamp {at}, expected ~{W}"
);
}
/// B3: an estimate below the floor (here 2001) is not adopted.
#[test]
fn below_floor_observed_at_is_not_adopted() {