From 8108feb1465724f38a212fa74542aa3b9fa303dc Mon Sep 17 00:00:00 2001 From: "K. Isom" Date: Fri, 16 Oct 2026 18:05:52 -0700 Subject: [PATCH] feat(core): workspace, Observation, content-derived id --- Cargo.lock | 220 ++++++++++++++++++++++++++++++++++ Cargo.toml | 3 + rust-toolchain.toml | 2 + scripts/check-no-std.sh | 4 + tines-core/Cargo.toml | 14 +++ tines-core/src/consts.rs | 31 +++++ tines-core/src/lib.rs | 13 ++ tines-core/src/observation.rs | 125 +++++++++++++++++++ tines-sim/Cargo.toml | 4 + tines-sim/src/lib.rs | 0 10 files changed, 416 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 rust-toolchain.toml create mode 100755 scripts/check-no-std.sh create mode 100644 tines-core/Cargo.toml create mode 100644 tines-core/src/consts.rs create mode 100644 tines-core/src/lib.rs create mode 100644 tines-core/src/observation.rs create mode 100644 tines-sim/Cargo.toml create mode 100644 tines-sim/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8fa4bdf --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,220 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "serde", + "stable_deref_trait", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tines-core" +version = "0.1.0" +dependencies = [ + "blake2", + "heapless", + "postcard", + "serde", +] + +[[package]] +name = "tines-sim" +version = "0.1.0" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3cc5152 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["tines-core", "tines-sim"] +resolver = "2" diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/scripts/check-no-std.sh b/scripts/check-no-std.sh new file mode 100755 index 0000000..1b76258 --- /dev/null +++ b/scripts/check-no-std.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -e +rustup target add thumbv7em-none-eabihf +cargo build -p tines-core --no-default-features --target thumbv7em-none-eabihf diff --git a/tines-core/Cargo.toml b/tines-core/Cargo.toml new file mode 100644 index 0000000..428090a --- /dev/null +++ b/tines-core/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tines-core" +version = "0.1.0" +edition = "2021" + +[features] +default = ["alloc"] +alloc = [] + +[dependencies] +heapless = { version = "0.8", features = ["serde"] } +serde = { version = "1", default-features = false, features = ["derive"] } +postcard = { version = "1", default-features = false } +blake2 = { version = "0.10", default-features = false } diff --git a/tines-core/src/consts.rs b/tines-core/src/consts.rs new file mode 100644 index 0000000..120a7a1 --- /dev/null +++ b/tines-core/src/consts.rs @@ -0,0 +1,31 @@ +//! Compile-time constants for the tines workspace. +//! +//! Every value here is a compile-time `const` so it can drive generic +//! parameters (e.g. `heapless::Vec` capacities) and be optimized away. + +/// Maximum topic length in bytes. +pub const MAX_TOPIC: usize = 64; + +/// Maximum payload length in bytes. +pub const MAX_PAYLOAD: usize = 1024; + +/// Maximum number of peers a node tracks. +pub const MAX_PEERS: usize = 8; + +/// Maximum size, in bytes, of a per-key summary. +pub const SUMMARY_BYTES: usize = 512; + +/// Number of hash functions used by the per-key Bloom filter. +pub const BLOOM_K: usize = 3; + +/// Interval, in milliseconds, between summary emissions. +pub const SUMMARY_INTERVAL_MS: u64 = 2000; + +/// Timeout, in milliseconds, for inbound frame reassembly. +pub const REASSEMBLY_TIMEOUT_MS: u64 = 5000; + +/// Maximum number of frames buffered for outbound delivery. +pub const SEND_QUEUE_CAP: usize = 256; + +/// Maximum UDP frame size in bytes. +pub const MAX_FRAME: usize = 1500; diff --git a/tines-core/src/lib.rs b/tines-core/src/lib.rs new file mode 100644 index 0000000..ef66219 --- /dev/null +++ b/tines-core/src/lib.rs @@ -0,0 +1,13 @@ +//! tines-core: content-defined Observation identity and shared-memory core. +#![no_std] + +#[cfg(feature = "alloc")] +extern crate alloc; + +pub mod consts; +pub mod observation; + +pub use observation::{Observation, NodeId, ObservationId, Payload, Tick, Topic}; + +#[cfg(test)] +extern crate std; diff --git a/tines-core/src/observation.rs b/tines-core/src/observation.rs new file mode 100644 index 0000000..23e7e80 --- /dev/null +++ b/tines-core/src/observation.rs @@ -0,0 +1,125 @@ +//! Content-defined `Observation` identity. + +use crate::consts; +use blake2::{Blake2s256, Digest}; +use heapless::Vec; +use serde::{Deserialize, Serialize}; + +/// 16-byte node identifier. +pub type NodeId = [u8; 16]; +/// 16-byte content-derived observation identifier (first 16 bytes of Blake2s-256). +pub type ObservationId = [u8; 16]; +/// Monotonic sequence number / tick. +pub type Tick = u64; +/// Heapless, fixed-capacity topic. +pub type Topic = Vec; +/// Heapless, fixed-capacity payload. +pub type Payload = Vec; + +/// An observation published by a node, with content-derived identity. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Observation { + pub observer: NodeId, + pub seq: u64, + pub age_ms: u64, + pub observed_at: Option, + pub topic: Topic, + pub payload: Payload, +} + +impl Observation { + /// Content-derived identity: Blake2s-256 over + /// `(observer, seq, topic_len, topic, payload_len, payload)`, first 16 bytes. + /// + /// The hash ignores `age_ms` and `observed_at`, so two observations that + /// differ only in age/time are still treated as the same key. + pub fn id(&self) -> ObservationId { + let mut hasher = Blake2s256::new(); + hasher.update(self.observer); + hasher.update(self.seq.to_le_bytes()); + hasher.update((self.topic.len() as u16).to_le_bytes()); + hasher.update(&self.topic[..]); + hasher.update((self.payload.len() as u16).to_le_bytes()); + hasher.update(&self.payload[..]); + let digest = hasher.finalize(); + let mut id = [0u8; 16]; + id.copy_from_slice(&digest[..16]); + id + } + + /// Returns a copy with `age_ms` advanced by `elapsed_ms` (saturating). + pub fn aged(&self, elapsed_ms: u64) -> Observation { + Observation { + observer: self.observer, + seq: self.seq, + age_ms: self.age_ms.saturating_add(elapsed_ms), + observed_at: self.observed_at, + topic: self.topic.clone(), + payload: self.payload.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn obs(seq: u64, age: u64, at: Option) -> Observation { + Observation { + observer: [1; 16], + seq, + age_ms: age, + observed_at: at, + topic: Topic::from_slice(b"door/3").unwrap(), + payload: Payload::from_slice(b"open").unwrap(), + } + } + + #[test] + fn id_ignores_age_and_observed_at() { + assert_eq!(obs(1, 0, None).id(), obs(1, 999, Some(5)).id()); + } + + #[test] + fn id_changes_with_seq() { + assert_ne!(obs(1, 0, None).id(), obs(2, 0, None).id()); + } + + #[test] + fn id_changes_with_observer() { + let mut b = obs(1, 0, None); + b.observer = [2; 16]; + assert_ne!(obs(1, 0, None).id(), b.id()); + } + + #[test] + fn id_distinguishes_topic_payload_boundary() { + // topic "ab" payload "c" vs topic "a" payload "bc" must differ + let mut x = obs(1, 0, None); + x.topic = Topic::from_slice(b"ab").unwrap(); + x.payload = Payload::from_slice(b"c").unwrap(); + let mut y = obs(1, 0, None); + y.topic = Topic::from_slice(b"a").unwrap(); + y.payload = Payload::from_slice(b"bc").unwrap(); + assert_ne!(x.id(), y.id()); + } + + #[test] + fn aged_saturates() { + assert_eq!(obs(1, u64::MAX - 1, None).aged(10).age_ms, u64::MAX); + } + + #[test] + fn id_known_vector() { + // pin the hash layout: observer=[1;16], seq=7, topic="t", payload="p" + let mut o = obs(7, 0, None); + o.topic = Topic::from_slice(b"t").unwrap(); + o.payload = Payload::from_slice(b"p").unwrap(); + assert_eq!( + o.id(), + [ + 0x92, 0x37, 0x37, 0xa1, 0x06, 0x9b, 0xd9, 0x5e, 0x08, 0xc2, 0xe6, 0x39, 0x7d, 0x4a, 0x8f, 0xd0 + ] + ); + } +} diff --git a/tines-sim/Cargo.toml b/tines-sim/Cargo.toml new file mode 100644 index 0000000..7bb0cc5 --- /dev/null +++ b/tines-sim/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "tines-sim" +version = "0.1.0" +edition = "2021" diff --git a/tines-sim/src/lib.rs b/tines-sim/src/lib.rs new file mode 100644 index 0000000..e69de29