release(core): tines-core 1.0.0
This commit is contained in:
@@ -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
@@ -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
@@ -1486,7 +1486,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tines-core"
|
||||
version = "0.3.0"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"blake2",
|
||||
"heapless 0.8.0",
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
//! tines-core: content-defined Observation identity and shared-memory core.
|
||||
#![no_std]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
extern crate alloc;
|
||||
|
||||
@@ -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
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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"] }
|
||||
|
||||
Reference in New Issue
Block a user