release(core): tines-core 1.0.0

This commit is contained in:
2026-10-22 08:44:09 -07:00
parent 164cf97a5b
commit 8425232ba1
15 changed files with 145 additions and 27 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ architecture in the **spec first**:
- **Addressed delivery (authoritative):** `docs/superpowers/specs/2026-10-18-tines-addressed-delivery.md` - **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 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. - **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. - **Host hardening (next, hosts):** `docs/superpowers/specs/2026-10-19-tines-host-hardening.md` — findings F10–F12.
- **Implementation plans:** `docs/superpowers/plans/` - **Implementation plans:** `docs/superpowers/plans/`
+42 -4
View File
@@ -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 [Semantic Versioning](https://semver.org/); before 1.0, a minor bump may break
the API. the API.
## Unreleased ## 1.0.0 — 2026-10-21
The fleet-deployment milestone and the fleet fixes (security assessment tines-core 1.0: the wire format is frozen (wire v1), the public API is
findings F1–F5 and F17; see `THREAT_MODEL.md`). The workspace version is narrowed and documented, and the core hardening from the 1.0 spec is in.
still 0.3.0. 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<Dest>` where `Dest { to, lifetime_ms }`; the lifetime is
excluded from observation identity.
- **Signature slot** (tines-core). `Observation` carries a reserved
`sig: Option<Signature>` (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 ### Added
Generated
+1 -1
View File
@@ -1486,7 +1486,7 @@ dependencies = [
[[package]] [[package]]
name = "tines-core" name = "tines-core"
version = "0.3.0" version = "1.0.0"
dependencies = [ dependencies = [
"blake2", "blake2",
"heapless 0.8.0", "heapless 0.8.0",
+26 -8
View File
@@ -351,14 +351,11 @@ for event in node.drain_events() {
} }
// Keep the store bounded. // Keep the store bounded.
node.trim( let mut policy = TrimPolicy::default();
&TrimPolicy { policy.keep_per_topic = Some(4);
keep_per_topic: Some(4), policy.max_age_ms = Some(3_600_000);
max_age_ms: Some(3_600_000), policy.max_count = Some(10_000);
max_count: Some(10_000), node.trim(&policy, 500);
},
500,
);
``` ```
`observe` rejects an oversized topic or payload with `ObserveError::TooLarge`; `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 GitHub Actions runs the same script on every push to `master` and on every
pull request. 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 ## Security
tines has **no trust layer yet**, by design. Anyone in radio range can read tines has **no trust layer yet**, by design. Anyone in radio range can read
+43 -6
View File
@@ -57,11 +57,22 @@ threat questions (§8) before designing signing.
| F5 | Fixed | `4718b6f`, `24b5a78` | | F5 | Fixed | `4718b6f`, `24b5a78` |
| F17 | Fixed | `6e2dc6f` | | 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: Still open:
- F6, F8, F9 and F13: the 1.0 spec §10.
- F10–F12: the host-hardening spec. - F10–F12: the host-hardening spec.
- F7 and F14: 1.1. - 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 ## 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 advances only while observations arrive, so a robot that is mostly
offline, the normal case, may never flush. offline, the normal case, may never flush.
- tines-ros2 never calls `flush` or `trim`. - tines-ros2 never calls `flush` or `trim`.
- Even when it runs, `flush` writes `LogStore`'s own tick as the clock - Even when it runs, `flush` wrote `LogStore`'s own tick as the clock
marker, and that tick advances only when something new is inserted marker, and that tick advanced only when something new was inserted
(tines-store/src/log.rs:698). On a quiet robot the marker is stale, and (tines-store/src/log.rs:698). On a quiet robot the marker was stale, and
the time since the last insert is lost. 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:** - **Effect:**
- After a restart, every stored observation's age is folded only up to the - 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 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 on clean shutdown. The fleet-fixes spec asked for "a scheduled flush". The
trim defaults belong to host hardening (F10). 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 ## 7. Threats by STRIDE, mapped to findings
| | Threat | Status today | Findings | Planned mitigation | | | Threat | Status today | Findings | Planned mitigation |
+1 -1
View File
@@ -12,7 +12,7 @@ keywords = ["robotics", "gossip", "delay-tolerant", "ble", "bluer"]
categories = ["science::robotics", "network-programming"] categories = ["science::robotics", "network-programming"]
[dependencies] [dependencies]
tines-core = { path = "../tines-core", version = "0.3.0" } tines-core = { path = "../tines-core", version = "1" }
bluer = { version = "0.17", features = ["bluetoothd", "l2cap"] } bluer = { version = "0.17", features = ["bluetoothd", "l2cap"] }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "sync", "net", "time", "signal"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "sync", "net", "time", "signal"] }
futures = "0.3" futures = "0.3"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "tines-core" name = "tines-core"
version.workspace = true version = "1.0.0"
rust-version = "1.82.0" rust-version = "1.82.0"
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
+1
View File
@@ -10,6 +10,7 @@ use crate::observation::{NodeId, Observation};
/// Reduces an ordered stream of observations to a single accumulated belief. /// Reduces an ordered stream of observations to a single accumulated belief.
pub trait BeliefPolicy { pub trait BeliefPolicy {
/// The accumulated belief this policy folds into.
type Belief; type Belief;
/// Streaming fold: called once per matching observation (already /// Streaming fold: called once per matching observation (already
/// age-adjusted to `now`), in no particular order. `acc` is `None` before /// age-adjusted to `now`), in no particular order. `acc` is `None` before
+1
View File
@@ -1,5 +1,6 @@
//! tines-core: content-defined Observation identity and shared-memory core. //! tines-core: content-defined Observation identity and shared-memory core.
#![no_std] #![no_std]
#![deny(missing_docs)]
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
extern crate alloc; extern crate alloc;
+4
View File
@@ -42,7 +42,9 @@ pub struct Dest {
#[non_exhaustive] #[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Observation { pub struct Observation {
/// The node that published this observation.
pub observer: NodeId, pub observer: NodeId,
/// The observer's per-observation sequence number.
pub seq: u64, pub seq: u64,
/// The observer's elapsed time for this observation, in ms. /// 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 /// this observation was published, or `None` if the observer has no
/// trusted clock. Set once at construction and never changed by a relay. /// trusted clock. Set once at construction and never changed by a relay.
pub observed_at: Option<u64>, pub observed_at: Option<u64>,
/// The topic this observation is published under.
pub topic: Topic, pub topic: Topic,
/// The observation's content.
pub payload: Payload, pub payload: Payload,
/// Addressing: `None` is a broadcast, `Some(dest)` is addressed to /// Addressing: `None` is a broadcast, `Some(dest)` is addressed to
/// `dest.to` and expires per `dest.lifetime_ms`. /// `dest.to` and expires per `dest.lifetime_ms`.
+20 -1
View File
@@ -21,24 +21,40 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub enum Frame { pub enum Frame {
/// A broadcast bloom summary of a node's observation-id set.
Summary { Summary {
/// The sending node.
from: NodeId, from: NodeId,
/// The bloom salt the bits were hashed with.
salt: u32, salt: u32,
/// The broadcast bloom bits (the summary body).
bits: Vec<u8, { consts::SUMMARY_BYTES }>, bits: Vec<u8, { consts::SUMMARY_BYTES }>,
}, },
/// A single observation, broadcast or addressed (see [`Observation::dest`]).
Obs { Obs {
/// The sending (relaying or originating) node.
from: NodeId, from: NodeId,
/// The carried observation.
obs: Observation, obs: Observation,
}, },
/// One fragment of a large addressed observation (reassembly by `id`).
Frag { Frag {
/// The sending node.
from: NodeId, from: NodeId,
/// The observation this fragment belongs to.
id: ObservationId, id: ObservationId,
/// This fragment's 0-based position in the reassembly.
index: u8, index: u8,
/// The total number of fragments for `id`.
count: u8, count: u8,
/// The fragment payload bytes.
data: Vec<u8, { consts::MAX_FRAME }>, data: Vec<u8, { consts::MAX_FRAME }>,
}, },
/// An acknowledgement of stored observation ids (custody-chain release).
Ack { Ack {
/// The acknowledging node.
from: NodeId, from: NodeId,
/// The observation ids being acknowledged.
ids: Vec<ObservationId, { consts::ACK_IDS_CAP }>, ids: Vec<ObservationId, { consts::ACK_IDS_CAP }>,
}, },
} }
@@ -139,7 +155,10 @@ pub enum WireError {
/// The input bytes were malformed, truncated, or not a valid `Frame`. /// The input bytes were malformed, truncated, or not a valid `Frame`.
Malformed, Malformed,
/// The frame's leading `WIRE_VERSION` byte did not match `consts::WIRE_VERSION`. /// 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)] #[cfg(test)]
+1 -1
View File
@@ -20,7 +20,7 @@ default = []
ros2 = ["dep:rclrs", "dep:ros-env", "dep:tines"] ros2 = ["dep:rclrs", "dep:ros-env", "dep:tines"]
[dependencies] [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-store = { path = "../tines-store", version = "0.3.0" }
tines-bt = { path = "../tines-bt", 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 # The host's wall-clock abstraction (`HostClock`) and the trusted-time state
+1 -1
View File
@@ -12,7 +12,7 @@ keywords = ["robotics", "gossip", "simulation", "delay-tolerant"]
categories = ["simulation", "science::robotics"] categories = ["simulation", "science::robotics"]
[dependencies] [dependencies]
tines-core = { path = "../tines-core", version = "0.3.0" } tines-core = { path = "../tines-core", version = "1" }
rand = "0.8" rand = "0.8"
[dev-dependencies] [dev-dependencies]
+1 -1
View File
@@ -12,7 +12,7 @@ keywords = ["robotics", "gossip", "delay-tolerant", "store", "persistent"]
categories = ["science::robotics"] categories = ["science::robotics"]
[dependencies] [dependencies]
tines-core = { path = "../tines-core", version = "0.3.0" } tines-core = { path = "../tines-core", version = "1" }
postcard = { version = "1", features = ["use-std"] } postcard = { version = "1", features = ["use-std"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
+1 -1
View File
@@ -11,7 +11,7 @@ keywords = ["robotics", "delay-tolerant", "science::robotics", "ipc", "ble"]
categories = ["science::robotics", "network-programming"] categories = ["science::robotics", "network-programming"]
[dependencies] [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-store = { path = "../tines-store", version = "0.3.0" }
tines-bt = { path = "../tines-bt", version = "0.3.0" } tines-bt = { path = "../tines-bt", version = "0.3.0" }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }