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`
- **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/`
+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
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<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
Generated
+1 -1
View File
@@ -1486,7 +1486,7 @@ dependencies = [
[[package]]
name = "tines-core"
version = "0.3.0"
version = "1.0.0"
dependencies = [
"blake2",
"heapless 0.8.0",
+26 -8
View File
@@ -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
+43 -6
View File
@@ -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 |
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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;
+4
View File
@@ -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<u64>,
/// 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`.
+20 -1
View File
@@ -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<u8, { consts::SUMMARY_BYTES }>,
},
/// 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<u8, { consts::MAX_FRAME }>,
},
/// An acknowledgement of stored observation ids (custody-chain release).
Ack {
/// The acknowledging node.
from: NodeId,
/// The observation ids being acknowledged.
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`.
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)]
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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]
+1 -1
View File
@@ -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"] }
+1 -1
View File
@@ -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"] }