From 8425232ba118a28f3fbbcc38d82c0dcda24d5c3f Mon Sep 17 00:00:00 2001 From: "K. Isom" Date: Wed, 21 Oct 2026 21:06:03 -0700 Subject: [PATCH] release(core): tines-core 1.0.0 --- AGENTS.md | 2 +- CHANGELOG.md | 46 +++++++++++++++++++++++++++++--- Cargo.lock | 2 +- README.md | 34 ++++++++++++++++++------ THREAT_MODEL.md | 49 ++++++++++++++++++++++++++++++----- tines-bt/Cargo.toml | 2 +- tines-core/Cargo.toml | 2 +- tines-core/src/belief.rs | 1 + tines-core/src/lib.rs | 1 + tines-core/src/observation.rs | 4 +++ tines-core/src/wire.rs | 21 ++++++++++++++- tines-ros2/Cargo.toml | 2 +- tines-sim/Cargo.toml | 2 +- tines-store/Cargo.toml | 2 +- tines/Cargo.toml | 2 +- 15 files changed, 145 insertions(+), 27 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index cc9f6d02..e2bd64ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ architecture in the **spec first**: - **Addressed delivery (authoritative):** `docs/superpowers/specs/2026-10-18-tines-addressed-delivery.md` - **Fleet deployment v1 (authoritative):** `docs/superpowers/specs/2026-10-18-tines-fleet-deployment-design.md` — supersedes the addressed-delivery spec's epidemic forwarding (custody chain with handoff receipts). - **Fleet fixes (done):** `docs/superpowers/specs/2026-10-19-tines-fleet-fixes.md` — security-assessment findings F1–F5 and F17. -- **tines-core 1.0 (next, core):** `docs/superpowers/specs/2026-10-18-tines-1-0-design.md` — wire version, public API, signature slot, core hardening (§10). +- **tines-core 1.0 (done — shipped as tines-core 1.0.0, 2026-10-21):** `docs/superpowers/specs/2026-10-18-tines-1-0-design.md` — wire version, public API, signature slot, core hardening (§10). - **Host hardening (next, hosts):** `docs/superpowers/specs/2026-10-19-tines-host-hardening.md` — findings F10–F12. - **Implementation plans:** `docs/superpowers/plans/` diff --git a/CHANGELOG.md b/CHANGELOG.md index b03e7c45..8f5cf9a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,49 @@ All notable changes to the tines crates. Versions follow [Semantic Versioning](https://semver.org/); before 1.0, a minor bump may break the API. -## Unreleased +## 1.0.0 — 2026-10-21 -The fleet-deployment milestone and the fleet fixes (security assessment -findings F1–F5 and F17; see `THREAT_MODEL.md`). The workspace version is -still 0.3.0. +tines-core 1.0: the wire format is frozen (wire v1), the public API is +narrowed and documented, and the core hardening from the 1.0 spec is in. +This release ships the fleet-deployment milestone (handoff receipts) and the +remaining fleet fixes (F1–F5, F6, F8, F9, F13, F17; see `THREAT_MODEL.md`). + +### Breaking (from 0.3.0) + +- **Wire v1** (tines-core). Every frame now leads with a `WIRE_VERSION` byte + (4). 0.x-shaped frames are rejected as `WireError::VersionMismatch`. +- **`Dest` replaces `to`/`lifetime_ms`** (tines-core). Addressed delivery now + takes `dest: Option` where `Dest { to, lifetime_ms }`; the lifetime is + excluded from observation identity. +- **Signature slot** (tines-core). `Observation` carries a reserved + `sig: Option` (algorithm + up to 64 bytes). 1.0 enforces the + wire shape but does not sign or verify (1.1). +- **Public API narrowing** (tines-core). Nine items moved `pub(crate)`, + eleven types are `#[non_exhaustive]` (build via constructors, not struct + literals). `Frame` and `WireError` are now *public*: the wire format is the + 1.x compatibility contract. +- **`Store` trait settled** (tines-core). `flush(&mut self, now)` and + `is_empty` joined the trait; `Node::store_mut` is gone. A new opt-in + `WallClockStore` trait carries the trusted-time operations + (`set_wall_clock`, `peer_time_estimate`) that only `LogStore` implements. +- **`NodeEvent` + counted drops** (tines-core). Exchange outcomes surface as + `NodeEvent` (`Delivered`, `Dropped { reason, count }`) through + `Node::drain_events`; `DropReason` is `VersionMismatch`, `Malformed`, or + `IdentityChanged`. +- **`Node::poll` is the only pump** (tines-core). The separate custody + re-offer pass folded into `poll`. +- **Log format v2** (tines-store). `FORMAT_VERSION` bumped for the new + `Observation` encoding; opening an older log is a hard error that names + the fix (delete the log). +- **Quarantine + wall-clock fold** (tines-store). `LogStore::open(path, now)` + takes no wall time; observations held across a power-off are quarantined + until the host feeds trusted time via `set_wall_clock`. +- **Trusted wall time in the hosts** (tines). A `trusted_clock = "system"` + config flag or a peer-derived estimate (smallest `observed_at + age_ms`) + anchors the wall clock once; observations carry `observed_at: None` until + then. +- **MSRV** (tines-core). `rust-version = "1.82"`; CI builds and tests + tines-core alone on it. ### Added diff --git a/Cargo.lock b/Cargo.lock index 4813df49..644fe3c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1486,7 +1486,7 @@ dependencies = [ [[package]] name = "tines-core" -version = "0.3.0" +version = "1.0.0" dependencies = [ "blake2", "heapless 0.8.0", diff --git a/README.md b/README.md index 68f73e8a..d62e04d4 100644 --- a/README.md +++ b/README.md @@ -351,14 +351,11 @@ for event in node.drain_events() { } // Keep the store bounded. -node.trim( - &TrimPolicy { - keep_per_topic: Some(4), - max_age_ms: Some(3_600_000), - max_count: Some(10_000), - }, - 500, -); +let mut policy = TrimPolicy::default(); +policy.keep_per_topic = Some(4); +policy.max_age_ms = Some(3_600_000); +policy.max_count = Some(10_000); +node.trim(&policy, 500); ``` `observe` rejects an oversized topic or payload with `ObserveError::TooLarge`; @@ -598,6 +595,27 @@ sh scripts/ci.sh GitHub Actions runs the same script on every push to `master` and on every pull request. +## Stability + +tines-core 1.0 guarantees, within the 1.x line: + +- **Wire:** every 1.x release speaks wire v1. Only new optional `Frame` + variants may be added, plus new `SigAlgo` ids and signature versions within + the 64-byte `MAX_SIG` budget. A wire-v2 change, including any post-quantum + signature or a `MAX_FRAME` above 1500, ships as tines-core 2.0. +- **Identity:** `Observation::id` never changes within a major version, and + should not change across majors unless the hash itself is broken. +- **API:** standard semver; MSRV bumps are minor. Public types are + `#[non_exhaustive]` where new fields are plausible; a public trait may only + gain methods with a default implementation (or a new opt-in trait — e.g. + `WallClockStore` for wall-clock folds, which `Store` itself never carries). +- **Local behaviour** (capacities, timings, scheduling order, which candidate + goes first) can change in a minor release as long as the protocol + properties hold: custody always keeps ≥ 1 holder, receipts never shorten + world state, and every frame fits `mtu()`. +- **Signatures:** 1.0 carries them; 1.1 signs and verifies them, without + breaking the wire or the API. + ## Security tines has **no trust layer yet**, by design. Anyone in radio range can read diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index d61e3d75..e32c425c 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -57,11 +57,22 @@ threat questions (§8) before designing signing. | F5 | Fixed | `4718b6f`, `24b5a78` | | F17 | Fixed | `6e2dc6f` | +**1.0 fixes, 2026-10-21** (through `941a7ce`): + +| # | Status | Commit | +|---|---|---| +| F6 | Fixed | `fb71810` | +| F8 | Fixed | `fb71810` | +| F9 | Fixed | `fb71810`, `a180ec2` | +| F13 | Fixed | `fb71810` | +| F18 | **Partly fixed**: the stale clock-marker tick is fixed (`Store::flush(now)` takes the caller's `now`); hosts still don't flush on a schedule | `c42f5de` | +| F19 | **Open** (new, 2026-10-21): peer-derived trusted time is unauthenticated | — | + Still open: -- F6, F8, F9 and F13: the 1.0 spec §10. - F10–F12: the host-hardening spec. - F7 and F14: 1.1. -- F18 (new) and the rest of F3: host work, not yet specced (§10). +- F18 (remaining): scheduled flushes, host work (§10). +- F19: 1.1 (signatures), or a local clock source before peers (§6). ## 2. System and trust boundaries @@ -482,10 +493,12 @@ inserts and trims (log.rs module docs, "Durability contract"). advances only while observations arrive, so a robot that is mostly offline, the normal case, may never flush. - tines-ros2 never calls `flush` or `trim`. -- Even when it runs, `flush` writes `LogStore`'s own tick as the clock - marker, and that tick advances only when something new is inserted - (tines-store/src/log.rs:698). On a quiet robot the marker is stale, and - the time since the last insert is lost. +- Even when it runs, `flush` wrote `LogStore`'s own tick as the clock + marker, and that tick advanced only when something new was inserted + (tines-store/src/log.rs:698). On a quiet robot the marker was stale, and + the time since the last insert was lost. **Fixed in 1.0** (`c42f5de`): + `Store::flush(now)` takes the caller's tick, so the marker is the true + current time at every flush. - **Effect:** - After a restart, every stored observation's age is folded only up to the last clock marker. Time since that marker is lost, so facts look fresher @@ -502,6 +515,30 @@ inserts and trims (log.rs module docs, "Durability contract"). on clean shutdown. The fleet-fixes spec asked for "a scheduled flush". The trim defaults belong to host hardening (F10). +### F19 — Peer-derived trusted time is unauthenticated (Medium, A0) + +*Added 2026-10-21, when the hosts gained the peer-time path (Task 10b).* +When a host has no `trusted_clock = "system"` source, it estimates wall time +from a peer's observation: `observed_at + age_ms` from the youngest such +observation, then folds it into the store once. Nothing authenticates that +estimate. + +- **Effect:** + - A forged time **ahead** of the true time only makes observations expire + early: the fold ages everything faster, addressed messages die sooner, + and `observed_at` stamps run fast. Availability loss, not integrity loss. + - A forged time **behind** the true time takes the "downtime unknown" + path (or under-folds), leaving the store quarantined longer or ages + younger — the same effect as F17/F18 by another route. + - Worst case: the robot's own `observed_at` stamps are wrong until a + local source corrects them, so every observation it publishes between + the forged fold and the correction carries a bad wall time. +- **Mitigation:** prefer a local source whenever one exists (the config + flag is exactly that); otherwise accept that a peer's time is a + lower-bound hint, not a trusted clock. Signing and verification (1.1) + closes it; a monotonic-host sanity check (reject a wall time behind the + last fold) would contain the "behind" case today. + ## 7. Threats by STRIDE, mapped to findings | | Threat | Status today | Findings | Planned mitigation | diff --git a/tines-bt/Cargo.toml b/tines-bt/Cargo.toml index 62ff70f6..67c0b572 100644 --- a/tines-bt/Cargo.toml +++ b/tines-bt/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["robotics", "gossip", "delay-tolerant", "ble", "bluer"] categories = ["science::robotics", "network-programming"] [dependencies] -tines-core = { path = "../tines-core", version = "0.3.0" } +tines-core = { path = "../tines-core", version = "1" } bluer = { version = "0.17", features = ["bluetoothd", "l2cap"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "sync", "net", "time", "signal"] } futures = "0.3" diff --git a/tines-core/Cargo.toml b/tines-core/Cargo.toml index a8c34d1e..9b6a0066 100644 --- a/tines-core/Cargo.toml +++ b/tines-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tines-core" -version.workspace = true +version = "1.0.0" rust-version = "1.82.0" edition.workspace = true license.workspace = true diff --git a/tines-core/src/belief.rs b/tines-core/src/belief.rs index 7e7deb4c..db0bd0ed 100644 --- a/tines-core/src/belief.rs +++ b/tines-core/src/belief.rs @@ -10,6 +10,7 @@ use crate::observation::{NodeId, Observation}; /// Reduces an ordered stream of observations to a single accumulated belief. pub trait BeliefPolicy { + /// The accumulated belief this policy folds into. type Belief; /// Streaming fold: called once per matching observation (already /// age-adjusted to `now`), in no particular order. `acc` is `None` before diff --git a/tines-core/src/lib.rs b/tines-core/src/lib.rs index 4d059052..a825e3ca 100644 --- a/tines-core/src/lib.rs +++ b/tines-core/src/lib.rs @@ -1,5 +1,6 @@ //! tines-core: content-defined Observation identity and shared-memory core. #![no_std] +#![deny(missing_docs)] #[cfg(feature = "alloc")] extern crate alloc; diff --git a/tines-core/src/observation.rs b/tines-core/src/observation.rs index aaa530f8..873d52df 100644 --- a/tines-core/src/observation.rs +++ b/tines-core/src/observation.rs @@ -42,7 +42,9 @@ pub struct Dest { #[non_exhaustive] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Observation { + /// The node that published this observation. pub observer: NodeId, + /// The observer's per-observation sequence number. pub seq: u64, /// The observer's elapsed time for this observation, in ms. /// @@ -56,7 +58,9 @@ pub struct Observation { /// this observation was published, or `None` if the observer has no /// trusted clock. Set once at construction and never changed by a relay. pub observed_at: Option, + /// The topic this observation is published under. pub topic: Topic, + /// The observation's content. pub payload: Payload, /// Addressing: `None` is a broadcast, `Some(dest)` is addressed to /// `dest.to` and expires per `dest.lifetime_ms`. diff --git a/tines-core/src/wire.rs b/tines-core/src/wire.rs index dfc77bdd..4322c4f1 100644 --- a/tines-core/src/wire.rs +++ b/tines-core/src/wire.rs @@ -21,24 +21,40 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[allow(clippy::large_enum_variant)] pub enum Frame { + /// A broadcast bloom summary of a node's observation-id set. Summary { + /// The sending node. from: NodeId, + /// The bloom salt the bits were hashed with. salt: u32, + /// The broadcast bloom bits (the summary body). bits: Vec, }, + /// A single observation, broadcast or addressed (see [`Observation::dest`]). Obs { + /// The sending (relaying or originating) node. from: NodeId, + /// The carried observation. obs: Observation, }, + /// One fragment of a large addressed observation (reassembly by `id`). Frag { + /// The sending node. from: NodeId, + /// The observation this fragment belongs to. id: ObservationId, + /// This fragment's 0-based position in the reassembly. index: u8, + /// The total number of fragments for `id`. count: u8, + /// The fragment payload bytes. data: Vec, }, + /// An acknowledgement of stored observation ids (custody-chain release). Ack { + /// The acknowledging node. from: NodeId, + /// The observation ids being acknowledged. ids: Vec, }, } @@ -139,7 +155,10 @@ pub enum WireError { /// The input bytes were malformed, truncated, or not a valid `Frame`. Malformed, /// The frame's leading `WIRE_VERSION` byte did not match `consts::WIRE_VERSION`. - VersionMismatch { version: u8 }, + VersionMismatch { + /// The version byte read off the wire. + version: u8, + }, } #[cfg(test)] diff --git a/tines-ros2/Cargo.toml b/tines-ros2/Cargo.toml index 9b00c96b..4ae76f6e 100644 --- a/tines-ros2/Cargo.toml +++ b/tines-ros2/Cargo.toml @@ -20,7 +20,7 @@ default = [] ros2 = ["dep:rclrs", "dep:ros-env", "dep:tines"] [dependencies] -tines-core = { path = "../tines-core", version = "0.3.0" } +tines-core = { path = "../tines-core", version = "1" } tines-store = { path = "../tines-store", version = "0.3.0" } tines-bt = { path = "../tines-bt", version = "0.3.0" } # The host's wall-clock abstraction (`HostClock`) and the trusted-time state diff --git a/tines-sim/Cargo.toml b/tines-sim/Cargo.toml index e17e146a..1bda08c9 100644 --- a/tines-sim/Cargo.toml +++ b/tines-sim/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["robotics", "gossip", "simulation", "delay-tolerant"] categories = ["simulation", "science::robotics"] [dependencies] -tines-core = { path = "../tines-core", version = "0.3.0" } +tines-core = { path = "../tines-core", version = "1" } rand = "0.8" [dev-dependencies] diff --git a/tines-store/Cargo.toml b/tines-store/Cargo.toml index 487d05d6..d7a0c691 100644 --- a/tines-store/Cargo.toml +++ b/tines-store/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["robotics", "gossip", "delay-tolerant", "store", "persistent"] categories = ["science::robotics"] [dependencies] -tines-core = { path = "../tines-core", version = "0.3.0" } +tines-core = { path = "../tines-core", version = "1" } postcard = { version = "1", features = ["use-std"] } serde = { version = "1", features = ["derive"] } diff --git a/tines/Cargo.toml b/tines/Cargo.toml index 60194bb7..b992b07b 100644 --- a/tines/Cargo.toml +++ b/tines/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["robotics", "delay-tolerant", "science::robotics", "ipc", "ble"] categories = ["science::robotics", "network-programming"] [dependencies] -tines-core = { path = "../tines-core", version = "0.3.0" } +tines-core = { path = "../tines-core", version = "1" } tines-store = { path = "../tines-store", version = "0.3.0" } tines-bt = { path = "../tines-bt", version = "0.3.0" } serde = { version = "1", features = ["derive"] }