Files
tines/CHANGELOG.md
T
kyleandClaude Opus 5.5 0cb1ad5360 docs: sweep README, CHANGELOG, THREAT_MODEL and AGENTS after the 1.0 review fixes
- CHANGELOG: tines-core 1.0.0 and the hosts' 0.4.0 are separate,
  unreleased sections. Every core API break is listed (the nine
  pub(crate) items, the private modules and the frame_sender path, the
  local constants per decision K, non_exhaustive types, store_mut). The
  new names (WallClockStore, Node::set_wall_clock, peer_time_estimate,
  flush, IdentityChanged) are called out. The host section covers
  tines::trust, the config changes, decision J, snapshots, and the
  review fixes.
- THREAT_MODEL: F6, F8, F9 and F13 are fixed with their commits. F18 is
  partly fixed: the store side and the flush on first trusted time are
  done, but timed and shutdown flushes are still open. F19 now records
  decision J and the plausibility bounds, and what remains. §9, §10 and
  §11 are updated to match.
- README: Dest and sig in the observation table; flush and is_empty in
  the store table; only the public wire constants are listed; a new
  "Trusted time" section with the config keys and the systemd ordering;
  the MSRV and fuzz CI jobs; frame_sender's new path.
- AGENTS: 1.0 is implemented but not yet published. The constants list
  shows the pub(crate) split, PEER_IDLE_MS, MIN_MTU's role and where
  MAX_FRAGMENTS lives. Notes the hidden conformance module.
- tines-bt README: frame_sender's path, and its own MAX_PEERS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00

427 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
All notable changes to the tines crates. Versions follow
[Semantic Versioning](https://semver.org/). `tines-core` is 1.x and keeps the
compatibility promise in README's "Stability" section. The host crates
(`tines`, `tines-store`, `tines-bt`, `tines-ros2`, `tines-sim`) stay 0.x,
where a minor bump may break the API.
## tines-core 1.0.0 — Unreleased
Implemented on `feat/tines-core-1-0`; not yet tagged or published. The wire
format is frozen as wire v1, the public API is narrowed and documented
(`#![deny(missing_docs)]`), and the core hardening from the 1.0 spec
(`docs/superpowers/specs/2026-10-18-tines-1-0-design.md` §10) is in. It also
carries the fleet-deployment milestone (handoff receipts) and the fleet fixes,
which were never released on their own. Findings F1, F6, F8, F9 and F13 are
fixed in core; see `THREAT_MODEL.md`.
### Breaking (from 0.3.0)
- **Wire v1.**
- Every frame now starts with a `WIRE_VERSION` byte (4). A 0.x-shaped frame
is dropped and counted as `DropReason::VersionMismatch`.
- The new `Ack` frame is the last `Frame` variant (see Added).
- Frames must be canonical. Trailing bytes after a complete frame, or after
a reassembled observation, are rejected and counted as `Malformed`.
- **`Dest` replaces `to`/`lifetime_ms`.**
- An addressed observation carries `dest: Option<Dest>`, where
`Dest { to, lifetime_ms }`.
- The identity hash covers `dest.to`. It still excludes `lifetime_ms`,
`age_ms` and `observed_at`.
- **Signature slot.**
- `Observation` carries `sig: Option<Signature>` (version, `SigAlgo`, and up
to `MAX_SIG` = 64 bytes).
- 1.0 checks the wire shape and carries signatures verbatim. It neither
signs nor verifies; that's 1.1. `sig` is outside the identity hash.
- **Nine items are now `pub(crate)`:**
- `Exchange` (and with it `Exchange::on_send_error`; drive the exchange
through `Node`);
- `Peer`;
- `InFlight`;
- `Bloom` (and its public `salt` field);
- `Reassembler`;
- `fragment`;
- `Frame`;
- `WireError`;
- `FRAG_OVERHEAD`.
- **The `bloom`, `exchange`, `frag` and `wire` modules are private**, so paths
through them are gone:
- `tines_core::wire::frame_sender` is now `tines_core::frame_sender`;
- `DropReason` and `NodeEvent` are reached at the crate root.
- **Local constants are `pub(crate)`** (spec decision K). They are defaults
that a later 1.x release will make configurable:
- `MAX_PEERS`, `SUMMARY_INTERVAL_MS`, `REASSEMBLY_TIMEOUT_MS`,
`SEND_QUEUE_CAP`, `EVENT_QUEUE_CAP`, `BCAST_SENT_CAP` and `ACK_BUFFER_CAP`
are no longer in `tines_core::consts`;
- the new local constants, `PEER_IDLE_MS`, `DROP_TABLE_CAP` and `MIN_MTU`,
were never public;
- only the wire constants stay public (see Added).
- `Transport::mtu`'s rustdoc now states the smallest MTU core can use (85
bytes to carry every valid observation).
- **Eleven types are `#[non_exhaustive]`.**
- Enums (match with a wildcard): `ObserveError`, `StoreError`, `SendError`,
`Event`, `NodeEvent`, `DropReason`, `SigAlgo`.
- Structs (no struct literals outside core): `Observation`, `Dest`,
`Signature`, `TrimPolicy`.
- Build an `Observation` with `Observation::broadcast` or
`Observation::addressed`, and a `TrimPolicy` with
`TrimPolicy::default()` plus field assignment.
- **`Node::drain_events` returns `NodeEvent<T::Addr>`.** This replaces the fleet
milestone's unreleased `ConfidenceEvent`.
- **`Node::store_mut` is removed.** It was added by the unreleased fleet fixes
so the service could reach `LogStore::flush`. Use `Node::flush`, and
`Node::set_wall_clock` / `Node::peer_time_estimate` for a `WallClockStore`.
Read access through `Node::store` stays.
- **MSRV.** `rust-version = "1.82.0"`. CI builds tines-core on 1.82, with and
without default features and for `thumbv7em-none-eabihf`. It runs no tests
there, because a dev-dependency needs edition 2024. From now on an MSRV bump
is a minor release.
### Added
- **Handoff receipts.** The new `Ack` frame carries up to `ACK_IDS_CAP` (90)
ids. Acks are sent as the exchange runs, and are accepted only from a peer
the id was sent to during the current contact.
- An ack for an addressed observation moves custody: the sender releases
its copy (`Store::release`) and never offers it again.
- An ack for a broadcast is confidence only. It surfaces as
`NodeEvent::Delivered { peer, id }` from `Node::drain_events`.
- **Counted drop reporting.** A frame this node can't use isn't dropped
silently.
- The exchange counts it under a `DropReason`: `VersionMismatch`,
`Malformed`, or `IdentityChanged` (F6). The count is keyed by the
sender's transport address.
- `Node::drain_events` reports each counter as
`NodeEvent::Dropped { addr, reason, count }`, after the queued
`Delivered` events.
- The counter table is bounded. When it's full, further drops go to one
overflow counter, reported with `addr: None` and the reason of the first
drop that overflowed since the last drain. Draining empties the table,
keys included.
- An expired addressed observation that's refused by policy is *not*
counted.
- **`Store::flush(now) -> Result<(), StoreError>`** and **`Store::is_empty`**,
both with safe defaults, plus **`Node::flush(now)`**. `flush` is the store's
durability point and its time anchor for ages after a restart. Flushing
twice is harmless, and an earlier `now` is clamped. Core never flushes on
its own.
- **`WallClockStore`** (spec decision H). An opt-in supertrait of `Store`, for
backends that hold data across power-off. It has an associated
`FoldReport`, `set_wall_clock(wall_now, now)` and `peer_time_estimate(now)`.
`LogStore` implements it; `MemStore` doesn't.
- **`Node::set_wall_clock`** and **`Node::peer_time_estimate`**. They exist
when `S: WallClockStore` and delegate to it, so no `&mut Store` escapes the
facade. Core forwards the wall time and never reads it.
- **`Dest`, `Signature` and `SigAlgo`**, and the constructors
`Observation::broadcast`, `Observation::addressed` and
`Observation::with_observed_at`. The two constructors panic on an
oversized topic or payload; `Node::observe` and `Node::observe_to` return
`ObserveError::TooLarge` instead.
- **`tines_core::frame_sender(bytes) -> Option<NodeId>`**: a frame's claimed
sender, for transports that bind a connection to an identity.
- **Public wire constants:** `WIRE_VERSION`, `MAX_TOPIC`, `MAX_PAYLOAD`,
`MAX_FRAME`, `SUMMARY_BYTES`, `ACK_IDS_CAP`, `MAX_SIG` and `BLOOM_K`.
- **`tines_core::store_conformance`**, behind the `conformance` feature and
`std`-free. A suite that lets any backend prove the §4 `Store` contract.
`MemStore` and `LogStore` both run it.
- The `conformance` feature also enables a `#[doc(hidden)]` `conformance`
module of frame encoders and local limits for the fuzz harness. It isn't
part of the supported API.
### Changed
- **Addressed delivery is a custody chain.** It used to be epidemic
forwarding.
- A message has one custodian at a time, which hands it on and releases it
when the next holder acks.
- A lost ack can leave two custodians for a while; the protocol never drops
a message.
- Bounce records carry a direction, so only a record of an actual send
validates an ack.
- **Core never trusts a backend's filtering.** It re-checks `dest` and
`is_released` on every custody candidate. It re-checks `dest` when building
a summary, rebuilding a broadcast diff, and sending a broadcast.
- **Core hardening** (spec §10):
- **F6:** a peer's `NodeId` is pinned by its first frame in a contact. A
later frame with another `from` is dropped and counted as
`IdentityChanged`. The pin resets on `Lost`, and on a repeat `Contact`
for a known peer.
- **F8:** a peer with no inbound frame for `PEER_IDLE_MS` (30 s) is evicted
as if `Lost`. That includes a peer that only ever sent `Contact`. Idle
peers are swept before a peer is added and at the start of every
`next_frame`, and a peer that resumes later is a new implicit contact.
- **F9:** at most one received Summary per peer is acted on per
`SUMMARY_INTERVAL_MS`. A summary is accepted once at least half the
interval has passed, to allow for poll jitter (spec decision L). The
send-queue rebuild skips ids the peer's filter says it holds
(`Bloom::might_contain`).
- **F13:**
- the event queue drops the oldest event when full;
- a removed peer's pending acks are purged, and only that peer's;
- frames, and reassembled observations, must be canonical.
- **Receipts are scoped to the contact.** Broadcast-send records for an
address are dropped when its contact starts or ends. A broadcast whose send
fails has its record undone, so its ack isn't accepted.
### Fixed
- A receiver whose store returned `Err` (for example `Full`) still acked, so
the sender released custody of a message nobody kept. Now nothing is acked
unless it was stored, addressed or broadcast (F1).
- From the 1.0 review:
- Silent and contact-only peers were never evicted, so eight spoofed
addresses could still lock out every newcomer (S1, F8).
- The drop table never freed its keys. After 32 distinct
`(addr, reason)` pairs, every drop went to the overflow slot until reboot
(S7).
- Losing one peer purged every peer's pending acks (S9).
- Broadcast receipts weren't limited to this contact, and a failed send
could still be confirmed (S10).
- A store whose broadcast iterator yielded addressed ids could leak them
into a summary or forward them as broadcasts (S11). A test store that
overrides no filtering now pins the re-checks (B8).
- A repeat `Contact` didn't reset the F6 pin.
- A reassembled observation was decoded with trailing bytes allowed.
- The summary rate limit could ignore an on-time summary measured a
millisecond early.
- `DropReason::IdentityChanged` carried a variant-level `#[non_exhaustive]`,
so code outside the crate couldn't match it.
## Hosts 0.4.0 — Unreleased
`tines`, `tines-store`, `tines-bt`, `tines-ros2` and `tines-sim` move to 0.4.0
and depend on `tines-core = "1"`. Like tines-core 1.0.0, nothing here is
tagged or published yet.
The 0.3.0 release shipped only `tines-core` and `tines-sim`. The four other
host crates were added afterwards, during the fleet milestone, and carried the
workspace version 0.3.0 in the tree without being released. The breaks below
are measured against that in-tree 0.3.0. This release also carries the fleet
milestone's host work (F2–F5, F17).
### Breaking
- **Log format v2** (tines-store).
- The file starts with a `"TINESLOG"` header and a format version (now 2,
for tines-core 1.0's `Observation` encoding).
- Each record has a sync marker, a length and a CRC-32C, and ids are
recomputed on load.
- Opening a log of any other version is an error that names the fix
(delete the log). There's no migration.
- **`LogStore::flush` is the `Store::flush(now)` trait method** (tines-store).
The inherent `flush()` is gone. Call `Node::flush(now)`; the marker now
uses the caller's tick.
- **`LogStore::open(path, now)` quarantines what it loads** (tines-store).
Observations already in the log are held back until the host feeds
trusted wall time (see Added).
- **`tines::serve`** takes a fifth argument, `peer_time_window_ms`.
**`tines::handle`** takes `observed_at: Option<u64>` before `now`.
- **Service config** (tines):
- unknown keys are rejected (`deny_unknown_fields`);
- `trusted_clock` accepts only `"system"`; any other value is a config
error;
- the `first_seq` field is gone (seqs come from the store's boot counter).
### Added
- **Trusted wall time in the hosts** (spec §9, decisions G and J). A host
opens its store before it knows the time, then adopts a wall time once per
run.
- **Local source first.** The service's `trusted_clock = "system"`, or
tines-ros2's `trust_system_clock` / `trust_wall_clock`.
- **Otherwise a peer, by decision J.**
- The host collects `Node::peer_time_estimate` for `peer_time_window_ms`
(default 10 000) from the first plausible estimate.
- It then adopts the latest estimate.
- The store only offers estimates within its
`LogStoreBuilder::plausible_wall_ms` range, so a forged one can't hide
the honest ones.
- **Plausible range.** A wall time is adopted only if it's between
2026-01-01Z (`tines::trust::WALL_FLOOR_MS`) and about 2126
(`WALL_CEILING_MS`). That applies to the system clock and to peers.
- **Flush on trust.** Adopting the time flushes the store at once, so a
marker carrying the wall time reaches disk.
- Until trust, observations carry `observed_at: None`; after it, the
host's wall time.
- **`tines::trust`**: `WALL_FLOOR_MS`, `WALL_CEILING_MS`,
`PLAUSIBLE_WALL_MS`, `DEFAULT_PEER_TIME_WINDOW_MS`, `is_plausible_wall_ms`,
`system_wall_ms`, `adopt_wall_clock` and `PeerTimeWindow`. `HostClock`
gains `anchor_wall`, `wall_ms` and `is_trusted`.
- **Service config keys** (tines):
- `trusted_clock`;
- `peer_time_window_ms` (default 10000);
- `unknown_downtime_age_ms` (default: the LogStore default, 24 h).
- **`TinesNode` trust methods** (tines-ros2): `trust_system_clock`,
`trust_wall_clock`, `set_peer_time_window_ms` and `has_trusted_time`. They
give a ROS2-only fleet a local trusted-time source.
- **LogStore quarantine and fold** (tines-store).
- `LogStore` implements `WallClockStore`.
- `set_wall_clock` folds the downtime into held ages, once, and returns a
`FoldReport`, which `fold_report` reads back later.
- **Downtime known:** held ages gain the wall-clock downtime.
- **Downtime unknown:** held addressed observations expire, and held
broadcasts are floored at `unknown_downtime_age_ms`.
- Quarantined entries are exempt from every trim rule (spec decision L).
- `peer_time_estimate` returns the latest estimate among observations
stored since this open.
- **`LogStoreBuilder`** (tines-store):
- `set_unknown_downtime_age_ms` (default 24 h);
- `plausible_wall_ms` (default unbounded; both hosts pass
`tines::trust::PLAUSIBLE_WALL_MS`).
- **Snapshots** (tines-store, spec decision I).
- `open`, after its tick fold, rewrites the log as a snapshot. So does
`set_wall_clock`, after its fold. A snapshot holds every live observation
at its current age, a `Release` for every released id still held, and a
closing clock marker.
- The snapshot goes to `<log>.snapshot-tmp` in the same directory. That
file is fsynced, locked, renamed over the log, and then the directory is
fsynced. A leftover temp file is removed on open.
- The fold's results and age lineage then survive any number of reboots.
Each snapshot also compacts the log.
- **New host crates** (first released in this line, see above):
- `tines-store`: `LogStore`, a persistent backend;
- `tines-bt`: a BLE transport over BlueZ;
- `tines`: a standalone service with Unix-socket JSON IPC;
- `tines-ros2`: a ROS2 node, with rclrs behind the `ros2` feature.
### Changed
- `LogStore` persists a boot counter, and `first_seq()` returns
`boot_count << 32`. Both hosts seed `Node::new` from it.
- `LogStore::release` writes a `Release` record immediately.
- `LogStore::open` takes an exclusive lock and creates the file `0600`.
- `LogStore::flush` and `set_wall_clock` clamp an earlier `now` to the store's
time anchor, so `stored_at` and clock markers never move backwards.
- A log whose first header was torn while it was being created (shorter than
the header, starting with the magic) opens as a new log. Any other short
file is still refused.
- tines-bt rewrite:
- L2CAP SeqPacket channels, one frame per packet.
- Every node advertises and scans; the smaller `NodeId` dials and the
larger accepts.
- Connections are bound to the advertised `NodeId`, and frames whose
claimed sender differs (`tines_core::frame_sender`) are dropped.
- Connections emit `Contact` and `Lost`.
- Live connections are capped by tines-bt's own `MAX_PEERS` (8).
- tines-sim's property tests run 64 cases again, and the bounce-overflow
property checks that the table really overflowed.
### Fixed
- The standalone service never polled its `Node`, so it never exchanged
anything. It now runs the exchange on a poll loop (F2).
- tines-bt had no listener, lost frame boundaries on a stream socket, and
stamped every frame with the configured peer (F3).
- Sequence numbers repeated after every restart, and released flags were
never persisted (F4).
- One corrupt record in the middle of the log truncated everything after it.
Recovery now skips bad records and truncates only a torn tail, with an
fsync of the file and its directory (F5).
- The service's tick was wall-clock *seconds*. Both hosts now use one
monotonic millisecond clock (F17).
- From the 1.0 review:
- `LogStore` didn't implement `Store::flush`, so `Node::flush` wrote no
marker and fsynced nothing (B1).
- Fold results and age lineage lived only in memory. An expired message
came back after the next reboot, and ages reset on a third boot (B2).
Snapshots fix both.
- A forged `observed_at = u64::MAX` overflowed: a panic in debug, a time
near 1970 in release (B3). The arithmetic saturates, and the hosts
bound every wall time they adopt.
- A flush between open and the fold made the fold count that time twice
(S2).
- `flush` didn't clamp an earlier `now` (S3).
- tines-ros2 had no local trusted-time source (S4).
- A quiet reboot lost everything, because nothing flushed after trust
was set (S5).
- The service trusted a 1970 system clock and ignored config typos (S6).
### Known issues
- The service talks to exactly one configured `peer`. The multi-peer
allowlist the fleet-fixes spec describes isn't wired up (F3).
- Neither host flushes on a timer or at shutdown (F18).
- The service trims and flushes only after 64 poll rounds that each stored
something new.
- tines-ros2 flushes only when it adopts trusted time, and never trims.
- Peer time is unauthenticated until 1.1. An adopted time isn't corrected
while the host runs (F19).
## 0.3.0 — 2026-10-18
### Breaking
- `Store` now requires `release(&mut self, id)` and
`is_released(&self, id) -> bool`. Implementations of `Store` — including test
doubles and future backends — must provide them.
### Added
- `Store::release` marks an observation as sent, so it is no longer offered for
exchange. `MemStore` tracks a per-entry `released` flag, and its
`for_each_addressed_id` skips released ids as an optimization (the exchange
predicate remains the load-bearing check). Releasing an id that is not stored
is a no-op.
## 0.2.1 — 2026-10-18
### Changed
- Package metadata only: `repository`, `documentation`, `readme`,
`keywords` and `categories`, shared through `[workspace.package]`. The
README now appears on crates.io.
- Each crate now ships the Apache-2.0 `LICENSE` text.
## 0.2.0 — 2026-10-18
Hardening from a full-codebase review. Wire-compatible with 0.1.0 except
where noted.
### Added
- `Exchange::on_send_error`: rolls back the last `next_frame` after a failed
send. `Node::poll` calls it, so a `Busy` or `Unreachable` frame is retried
instead of being lost.
- `Store::for_each_addressed_id`, with a default that visits every id.
`MemStore` overrides it, so custody no longer reads every broadcast
observation on each poll.
- `Bloom::fold`, and `Bloom::from_parts` now accepts any power-of-two length up
to `SUMMARY_BYTES`. Summaries are folded to fit the transport's MTU; a
0.1.0 node rejects a folded summary, so mixed-version links only sync when
the MTU allows a full 512-byte filter.
- `InFlight`, describing a fragment stream in progress.
### Changed
- **Breaking:** `Peer::in_flight` is now `Option<InFlight>` (was
`Option<(ObservationId, u8)>`).
- Custody (addressed) and broadcast sending alternate per peer, and addressed
candidates are taken in rotation.
- Bounce suppression lasts one contact, as the addressed-delivery spec says.
### Fixed
- A fragment stream could panic, or send corrupt fragments, when the
observation's age changed encoded length mid-stream or the tick went
backwards.
- Summaries larger than the MTU stopped all broadcast sync on narrow links.
- A full bounce table silently stopped recording, so one addressed message
was re-sent on every poll and starved broadcast.
- An addressed send interrupted another peer's fragment stream, which was
then never finished.
- An addressed message that couldn't fit the MTU blocked every other
addressed message for that peer.
- A duplicate addressed message was forwarded back to the carrier that sent
it.
### CI
- Tests, clippy and docs also run without the `alloc` feature; doc warnings
fail the build; `--locked` everywhere; toolchain pinned to 1.99.0; actions
pinned to commit SHAs.
## 0.1.0
Initial release: `tines-core` and `tines-sim`.