Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ Interval Tree Clock library (`crates/before-viz` visualizes the clocks).
`conformance` cargo feature ships the public validation suite for
caller-built links; `design/streaming-wire-deadlock.md` records why the
contract exists and the deadlock analysis behind it.
- The tree (sparse Merkle radix trie, path compression, content-addressed
- The tree (sparse Merkle radix trie, path compression, version-addressed
leaves, the memo/version-bounds design): module docs in `src/tree.rs` and
`src/tree/typed/`.
- The mirror protocols: module docs in `src/tree/mirror/` — `alternating/`
(V1, full-level alternation; the streaming protocol's behavioral oracle)
and `streaming/` (V2, fixed-memory; its module doc maps the layers:
backend materiality, the type-level phase schedule, the walk and the
proxy, the window, the wire vocabulary, the leaf conversion boundary).
- ITC semantics (`Party`, `Version`, `Clock`, the Law of Disjointness):
- ITC semantics (`Party`, `Version`, `Clock`, party disjointness):
`before`'s crate docs and `crates/before/CLAUDE.md`.

## Commands
Expand Down Expand Up @@ -152,12 +152,15 @@ wants `wasm-pack` and node/npm.
explicitly.
To re-accept deliberately: `just test-all`, then `cargo insta review`
(install: `cargo install cargo-insta`), then commit the updated
`tests/snapshots/*.snap`. One sanctioned exception for tamper sweeps
attributing snapshot history: the bookmark `frame_non_trivial` pin
(`src/bookmark/format/`) is a ratified *fixture re-pin* — its fixture
deliberately carries nested versions so the pin exercises real skyline
payload bytes, and the format is attested unchanged by the untouched
`frame_empty` pin and the round-trip/corruption suite. Attribute that
pin's history to the fixture, never to a protocol change.
`tests/snapshots/*.snap`. For the bookmark pins (`src/bookmark/format/`),
one narrower class exists for tamper sweeps attributing snapshot
history: a *fixture re-pin*, where only `frame_non_trivial` moves (its
fixture deliberately carries nested versions so the pin exercises real
skyline payload bytes) while the on-disk format is attested unchanged by
an untouched `frame_empty` pin and the round-trip/corruption suite.
Attribute such a re-pin to the fixture. A re-accept that moves
`frame_empty` is a bookmark *format* change — versioned by
`BOOKMARK_FORMAT_VERSION`, owner-ruled, and named in the re-accepting
commit like any other deliberate format change.
- Redaction leaves no tombstones: deletion-honoring rides on version bounds.
When reasoning about it, think version ceilings/floors, not markers.
68 changes: 5 additions & 63 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,13 @@ protocol-v1 = []
meter = ["before/limb-meter", "before/scan-meter"]

[dependencies]
before = { path = "crates/before", features = ["borsh"] }
bytes = "1"
before = { path = "crates/before", features = ["serde"] }
bytes = { version = "1", features = ["serde"] }
blake3 = "1.8"
static_assertions = "1.1"
itertools = "0.14"
borsh = { version = "1.6", features = ["bytes", "derive", "de_strict_order"] }
serde = { version = "1", features = ["derive"] }
ciborium = "0.2"
seq-macro = "0.3"
smallvec = { version = "1.15", features = ["union"] }
tinyvec = { version = "1.11", features = ["alloc"] }
Expand All @@ -78,7 +79,7 @@ rumors = { path = ".", features = ["test-internals", "conformance"] }
# The meter feature lights before's instrument surface for this crate's
# own tests: the conservation suite reads the exact-bit-length observation
# (`encoded_bits`) it denominates identity conservation in.
before = { path = "crates/before", features = ["borsh", "meter"] }
before = { path = "crates/before", features = ["serde", "meter"] }
proptest = "1"
criterion = { version = "0.5", features = ["html_reports"] }
insta = "1.47"
Expand Down
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ async fn main() -> Result<(), rumors::Error> {

// Convergence: Bob holds the message Alice sent before they ever met.
let snapshot = bob.snapshot();
let (_key, _version, message) = snapshot.iter().next().expect("one live message");
let (_version, message) = snapshot.iter().next().expect("one live message");
println!("bob heard: {message}");
// Prints exactly:
// bob heard: the meeting is at noon
Expand All @@ -185,9 +185,9 @@ async fn main() -> Result<(), rumors::Error> {
## How should you observe messages?

- `Snapshot` (`Rumors::snapshot`) is a **point-in-time value**:
iterate it, look up a `Key` (`Snapshot::get`), or slice it by
causal range (`Snapshot::range`). Taking one is cheap and never
waits.
iterate it, look up a message by its `Version` (`Snapshot::get`),
or slice it by causal range (`Snapshot::range`). Taking one is
cheap and never waits.
- `UnorderedMessages` (`Rumors::unordered_messages`) is the **live stream, arbitrary
order**: everything not already inside your starting checkpoint, then
everything learned afterwards, at the lowest cost. Use it by default.
Expand Down Expand Up @@ -234,6 +234,19 @@ the caller. The I/O traits are Tokio's runtime-independent
`AsyncRead` and `AsyncWrite`;
no Tokio runtime, spawning, sockets, or timers are required by this crate.

## Message payloads

Your message type `T` needs `serde::Serialize` and
`serde::de::DeserializeOwned`; payloads travel and are cached as
CBOR ([RFC 8949](https://www.rfc-editor.org/rfc/rfc8949)). Because
CBOR carries field and variant *names*, reordering struct fields or
enum variants does not change what peers understand: names are the
evolution contract (rename with `#[serde(rename)]` deliberately),
peers skip fields they don't know, and a missing field is an error
unless the type supplies `#[serde(default)]`. No canonical encoding
is required of `T`: a message's identity is the `Version` stamped
on it, never its bytes.

## Wire compatibility

Every session opens with a fixed 25-byte preamble carrying
Expand Down
28 changes: 14 additions & 14 deletions benches/gossip_fixed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group,
use rand::rngs::SmallRng;
use rand::seq::SliceRandom;
use rand::{RngCore, SeedableRng};
use rumors::{Key, Peer, Protocol, Rumors};
use rumors::{Peer, Protocol, Rumors, Version};

// The shared grid module exposes a superset of helpers; this bench only needs
// its sample-size policy so fixed-N runs line up with the existing benches.
Expand Down Expand Up @@ -264,9 +264,9 @@ fn build_bidir_redactions(protocol: Protocol, total_redactions: usize) -> (Rumor
assert!(total_redactions <= N / 2);
assert_eq!(total_redactions % 2, 0);

let (left, keys) = seeded_with_keys(protocol, N, 0xc786_a046_6b7d_c9d3);
let (left, versions) = seeded_with_versions(protocol, N, 0xc786_a046_6b7d_c9d3);
let right = grid::wire::bootstrap_fork(&left, protocol);
let shuffled = shuffled_keys(keys, 0x84f6_7932_1265_9eec ^ total_redactions as u64);
let shuffled = shuffled_versions(versions, 0x84f6_7932_1265_9eec ^ total_redactions as u64);
let per_side = total_redactions / 2;

redact_all(&left, &shuffled[..per_side]);
Expand All @@ -281,9 +281,9 @@ fn build_unilateral_redactions(
) -> (Rumors<u8>, Rumors<u8>) {
assert!(total_redactions <= N / 2);

let (left, keys) = seeded_with_keys(protocol, N, 0x2526_34f4_918f_e1c7);
let (left, versions) = seeded_with_versions(protocol, N, 0x2526_34f4_918f_e1c7);
let right = grid::wire::bootstrap_fork(&left, protocol);
let shuffled = shuffled_keys(keys, 0xd4f9_f46b_3c09_1d60 ^ total_redactions as u64);
let shuffled = shuffled_versions(versions, 0xd4f9_f46b_3c09_1d60 ^ total_redactions as u64);

redact_all(&left, &shuffled[..total_redactions]);

Expand All @@ -297,10 +297,10 @@ fn send_all(rumors: &Rumors<u8>, messages: Vec<u8>) {
}
}

fn redact_all(rumors: &Rumors<u8>, keys: &[Key]) {
fn redact_all(rumors: &Rumors<u8>, versions: &[Version]) {
let mut batch = rumors.batch();
for key in keys {
batch.redact(*key);
for version in versions {
batch.redact(version);
}
}

Expand All @@ -316,11 +316,11 @@ fn seeded_with_messages(protocol: Protocol, n: usize, seed: u64) -> Rumors<u8> {
rumors
}

fn seeded_with_keys(protocol: Protocol, n: usize, seed: u64) -> (Rumors<u8>, Vec<Key>) {
fn seeded_with_versions(protocol: Protocol, n: usize, seed: u64) -> (Rumors<u8>, Vec<Version>) {
let rumors = production_seed(protocol);
send_all(&rumors, random_bytes(n, seed));
let keys = rumors.snapshot().iter().map(|(k, _, _)| k).collect();
(rumors, keys)
let versions = rumors.snapshot().iter().map(|(v, _)| v.clone()).collect();
(rumors, versions)
}

fn warmed((left, right): (Rumors<u8>, Rumors<u8>)) -> (Rumors<u8>, Rumors<u8>) {
Expand All @@ -335,9 +335,9 @@ fn random_bytes(n: usize, seed: u64) -> Vec<u8> {
bytes
}

fn shuffled_keys(mut keys: Vec<Key>, seed: u64) -> Vec<Key> {
keys.shuffle(&mut SmallRng::seed_from_u64(seed));
keys
fn shuffled_versions(mut versions: Vec<Version>, seed: u64) -> Vec<Version> {
versions.shuffle(&mut SmallRng::seed_from_u64(seed));
versions
}

criterion_group!(benches, bench_gossip_fixed, bench_gossip_latency);
Expand Down
Loading
Loading