diff --git a/AGENTS.md b/AGENTS.md index 6d138933b..d551bf11e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ 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/` @@ -27,7 +27,7 @@ Interval Tree Clock library (`crates/before-viz` visualizes the clocks). 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 @@ -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. diff --git a/Cargo.lock b/Cargo.lock index ccd164de6..e556ad002 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -286,24 +286,10 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" dependencies = [ - "borsh-derive", "bytes", "cfg_aliases", ] -[[package]] -name = "borsh-derive" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "bumpalo" version = "3.20.3" @@ -327,6 +313,9 @@ name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] [[package]] name = "cast" @@ -1670,15 +1659,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -1974,8 +1954,8 @@ dependencies = [ "async-stream", "before", "blake3", - "borsh", "bytes", + "ciborium", "clap", "criterion", "futures", @@ -1989,6 +1969,7 @@ dependencies = [ "ratatui", "rumors", "seq-macro", + "serde", "smallvec", "static_assertions", "stats_alloc", @@ -2488,36 +2469,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - [[package]] name = "typenum" version = "1.20.1" @@ -2870,15 +2821,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" -dependencies = [ - "memchr", -] - [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index d47cfd61f..58e44c33d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } @@ -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" diff --git a/README.md b/README.md index 25ad0bf02..99009e10d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. @@ -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 diff --git a/benches/gossip_fixed.rs b/benches/gossip_fixed.rs index 44ae345ee..e9e44739f 100644 --- a/benches/gossip_fixed.rs +++ b/benches/gossip_fixed.rs @@ -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. @@ -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]); @@ -281,9 +281,9 @@ fn build_unilateral_redactions( ) -> (Rumors, Rumors) { 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]); @@ -297,10 +297,10 @@ fn send_all(rumors: &Rumors, messages: Vec) { } } -fn redact_all(rumors: &Rumors, keys: &[Key]) { +fn redact_all(rumors: &Rumors, versions: &[Version]) { let mut batch = rumors.batch(); - for key in keys { - batch.redact(*key); + for version in versions { + batch.redact(version); } } @@ -316,11 +316,11 @@ fn seeded_with_messages(protocol: Protocol, n: usize, seed: u64) -> Rumors { rumors } -fn seeded_with_keys(protocol: Protocol, n: usize, seed: u64) -> (Rumors, Vec) { +fn seeded_with_versions(protocol: Protocol, n: usize, seed: u64) -> (Rumors, Vec) { 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, Rumors)) -> (Rumors, Rumors) { @@ -335,9 +335,9 @@ fn random_bytes(n: usize, seed: u64) -> Vec { bytes } -fn shuffled_keys(mut keys: Vec, seed: u64) -> Vec { - keys.shuffle(&mut SmallRng::seed_from_u64(seed)); - keys +fn shuffled_versions(mut versions: Vec, seed: u64) -> Vec { + versions.shuffle(&mut SmallRng::seed_from_u64(seed)); + versions } criterion_group!(benches, bench_gossip_fixed, bench_gossip_latency); diff --git a/benches/in_memory.rs b/benches/in_memory.rs index 8d6c62022..c84c33483 100644 --- a/benches/in_memory.rs +++ b/benches/in_memory.rs @@ -3,7 +3,7 @@ //! These cover the operations that mutate or read a rumor set entirely in //! memory: everything except [`gossip`](rumors::Rumors::gossip), which //! serializes onto the wire (see `gossip_grid.rs` and `gossip_fixed.rs`). -//! The message payload is `()`, which borsh-encodes to zero bytes, so each +//! The message payload is `()`, whose encoding is one CBOR null byte, so each //! measurement reflects the tree / clock / hashing work rather than the cost //! of serializing a payload. //! @@ -25,7 +25,7 @@ //! - `batch_insert`: build a rumor set of size N from empty in one batch //! commit (insert throughput, averaged over the 0..N growth curve). //! - `iter`: a full live-message traversal of a size-N snapshot. -//! - `redact`: forget all N keys of a size-N set in one batch commit. +//! - `redact`: forget all N messages of a size-N set in one batch commit. //! - `range_delta`: iterate the causal delta of size D above a checkpoint in a //! size-N set — the version-bounds pruning claim: cost should track D //! plus the pruning frontier, not N. @@ -36,13 +36,13 @@ //! - `causal_replay` / `causal_delta`: the same two sweeps through a //! [`CausalMessages`] observer — the column-for-column price of causal //! delivery's rank-ordered staging over the plain passes. -//! - `get`: a point lookup by [`Key`] in a size-N set. +//! - `get`: a point lookup by [`Version`] in a size-N set. use std::hint::black_box; use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use futures::FutureExt; -use rumors::{CausalMessages, Key, Peer, Rumors, UnorderedMessages, causally}; +use rumors::{CausalMessages, Peer, Rumors, UnorderedMessages, Version, causally}; // The shared grid module exposes a superset of helpers; each bench binary uses // a subset, so the unused remainder is expected per-binary. @@ -64,12 +64,12 @@ fn send_units(rumors: &Rumors<()>, n: usize) { } /// A freshly seeded rumor set holding `n` messages, paired with its live -/// keys (in the snapshot's stable order). -fn build(n: usize) -> (Rumors<()>, Vec) { +/// versions (in the snapshot's stable order). +fn build(n: usize) -> (Rumors<()>, Vec) { let rumors: Rumors<()> = Peer::seed().into_rumors(); send_units(&rumors, n); - let keys = rumors.snapshot().iter().map(|(k, _, _)| k).collect(); - (rumors, keys) + let versions = rumors.snapshot().iter().map(|(v, _)| v.clone()).collect(); + (rumors, versions) } /// Drain everything `observer` has pending, without blocking, returning how @@ -114,7 +114,7 @@ fn bench_iter(c: &mut Criterion) { for &n in SIZES { group.sample_size(sample_size_for(n)); group.throughput(Throughput::Elements(n as u64)); - let (rumors, _keys) = build(n); + let (rumors, _versions) = build(n); let snapshot = rumors.snapshot(); group.bench_function(BenchmarkId::from_parameter(n), |b| { b.iter(|| { @@ -130,7 +130,7 @@ fn bench_iter(c: &mut Criterion) { group.finish(); } -/// `redact`: forget all N keys of a size-N set in a single batch commit. +/// `redact`: forget all N messages of a size-N set in a single batch commit. /// /// Each iteration redacts a fresh set built in untimed setup. `PerIteration` /// keeps only one tree alive at a time, which matters at N = 1M. @@ -142,10 +142,10 @@ fn bench_redact(c: &mut Criterion) { group.bench_function(BenchmarkId::from_parameter(n), |b| { b.iter_batched( || build(n), - |(rumors, keys)| { + |(rumors, versions)| { let mut batch = rumors.batch(); - for key in keys { - batch.redact(black_box(key)); + for version in &versions { + batch.redact(black_box(version)); } drop(batch); rumors @@ -214,7 +214,7 @@ fn bench_observer_replay(c: &mut Criterion) { for &n in SIZES { group.sample_size(sample_size_for(n)); group.throughput(Throughput::Elements(n as u64)); - let (rumors, _keys) = build(n); + let (rumors, _versions) = build(n); rumors.warm_caches(); group.bench_function(BenchmarkId::from_parameter(n), |b| { b.iter(|| { @@ -278,7 +278,7 @@ fn bench_causal_replay(c: &mut Criterion) { for &n in SIZES { group.sample_size(sample_size_for(n)); group.throughput(Throughput::Elements(n as u64)); - let (rumors, _keys) = build(n); + let (rumors, _versions) = build(n); rumors.warm_caches(); group.bench_function(BenchmarkId::from_parameter(n), |b| { b.iter(|| { @@ -317,7 +317,7 @@ fn bench_causal_delta(c: &mut Criterion) { group.finish(); } -/// `get`: a point lookup by key — one `O(depth)` descent, never a scan. +/// `get`: a point lookup by version — one `O(depth)` descent, never a scan. /// /// Lookups go through [`snapshot`](Rumors::snapshot), so the timed body pays /// for acquiring the root handle plus the descent: the whole per-call cost @@ -326,15 +326,15 @@ fn bench_get(c: &mut Criterion) { let mut group = c.benchmark_group("get"); for &n in SIZES { group.sample_size(sample_size_for(n)); - let (rumors, keys) = build(n); + let (rumors, versions) = build(n); rumors.warm_caches(); - // A fixed key from the middle of the stable iteration order; any - // live key costs the same depth-bounded descent. - let key = keys[keys.len() / 2]; + // A fixed version from the middle of the stable iteration order; + // any live version costs the same depth-bounded descent. + let version = versions[versions.len() / 2].clone(); group.bench_function(BenchmarkId::from_parameter(n), |b| { b.iter(|| { let snapshot = rumors.snapshot(); - black_box(snapshot.get(black_box(&key))); + black_box(snapshot.get(black_box(&version))); }) }); } diff --git a/benches/support/grid.rs b/benches/support/grid.rs index 9b2d3601c..5d3e76851 100644 --- a/benches/support/grid.rs +++ b/benches/support/grid.rs @@ -27,7 +27,7 @@ //! - small-delta = `common = n, differing = k, redacted = 0` (small `k`) //! - identical = `common = n, differing = 0, redacted = 0` -use rumors::{Key, Peer, Protocol, Rumors}; +use rumors::{Peer, Protocol, Rumors, Version}; #[path = "wire.rs"] pub mod wire; @@ -51,9 +51,9 @@ pub const DIFFERING: &[usize] = &[0, 1, 10, 100, 1_000, 10_000, 100_000]; /// (see the module docs). Bounded per cell by `common / 2`. pub const REDACTED: &[usize] = &[0, 1, 10, 100, 1_000, 10_000, 100_000]; -/// Commit `n` unit payloads to `rumors` as one batch. `()` borsh-encodes to -/// zero bytes, so fixtures measure tree / clock / hashing work, not payload -/// serialization. +/// Commit `n` unit payloads to `rumors` as one batch. `()` encodes as one +/// CBOR null byte, so fixtures measure tree / clock / hashing work, not +/// payload serialization. pub fn send_units(rumors: &Rumors<()>, n: usize) { let mut batch = rumors.batch(); for _ in 0..n { @@ -95,7 +95,7 @@ impl Cell { /// The dominant fixture-build cost: the messages inserted per side. Used to /// pick a sample count via [`sample_size_for`]. (Redactions only remove - /// keys, so they don't grow the tree.) + /// leaves, so they don't grow the tree.) pub fn build_magnitude(&self) -> usize { self.common + self.differing } @@ -152,10 +152,10 @@ pub fn build(cell: Cell) -> (Rumors<()>, Rumors<()>) { let left: Rumors<()> = Peer::seed().into_rumors(); send_units(&left, common); - // The shared prefix's keys, for carving the redaction blocks; order is - // immaterial (the blocks only need to be disjoint and deterministic, and - // the snapshot iterates in a stable order). - let shared: Vec = left.snapshot().iter().map(|(k, _, _)| k).collect(); + // The shared prefix's versions, for carving the redaction blocks; order + // is immaterial (the blocks only need to be disjoint and deterministic, + // and the snapshot iterates in a stable order). + let shared: Vec = left.snapshot().iter().map(|(v, _)| v.clone()).collect(); let right = wire::bootstrap_fork(&left, Protocol::V2); send_units(&left, differing); @@ -167,13 +167,13 @@ pub fn build(cell: Cell) -> (Rumors<()>, Rumors<()>) { // `cells` guarantees `common >= 2 * redacted`, so the slices don't // overlap and are in bounds. let mut batch = left.batch(); - for key in &shared[..redacted] { - batch.redact(*key); + for version in &shared[..redacted] { + batch.redact(version); } drop(batch); let mut batch = right.batch(); - for key in &shared[redacted..2 * redacted] { - batch.redact(*key); + for version in &shared[redacted..2 * redacted] { + batch.redact(version); } } diff --git a/benches/support/latency.rs b/benches/support/latency.rs index 09b0ed9d2..8f76bb73c 100644 --- a/benches/support/latency.rs +++ b/benches/support/latency.rs @@ -79,7 +79,6 @@ use std::sync::{Arc, Mutex}; use std::task::{Context, Poll, Waker}; use std::time::Duration; -use borsh::{BorshDeserialize, BorshSerialize}; use rumors::Rumors; use rumors::link::{Acceptor, Connector, Done, Link, STREAM_COUNT}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; @@ -432,7 +431,7 @@ impl DelayedWire { b: Rumors, ) -> ((Rumors, Rumors), Duration) where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { let wall_start = std::time::Instant::now(); let (pair, virtual_elapsed) = self.reconcile(a, b); @@ -471,7 +470,7 @@ impl DelayedWire { b: Rumors, ) -> ((Rumors, Rumors), Duration) where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { assert!( self.paused, @@ -484,7 +483,7 @@ impl DelayedWire { /// Drive one gossip session to completion, timing it in virtual time. fn reconcile(&mut self, a: Rumors, b: Rumors) -> ((Rumors, Rumors), Duration) where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { let Self { runtime, @@ -515,7 +514,7 @@ impl DelayedWire { #[allow(dead_code)] pub fn session_hops(capacity: usize, delay: Duration, (a, b): (Rumors, Rumors)) -> u32 where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { let mut wire = DelayedWire::new(capacity, delay); let (_pair, elapsed) = wire.round_trip_virtual(a, b); diff --git a/benches/support/wire.rs b/benches/support/wire.rs index 3eaebc7d8..c94585249 100644 --- a/benches/support/wire.rs +++ b/benches/support/wire.rs @@ -3,7 +3,6 @@ //! Benchmarks measure what ships: peers minted here run at the default //! pipeline window, which is the production budget in every build shape. -use borsh::{BorshDeserialize, BorshSerialize}; use rumors::link::MemoryLink; use rumors::{Peer, Protocol, Rumors}; @@ -26,7 +25,7 @@ impl Wire { /// Reconcile one pair while driving both endpoints concurrently. pub fn round_trip(&mut self, a: Rumors, b: Rumors) -> (Rumors, Rumors) where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { let (a_result, b_result) = pollster::block_on(async { tokio::join!(a.gossip(&mut self.a_link), b.gossip(&mut self.b_link)) @@ -40,7 +39,7 @@ impl Wire { /// Mint one disjoint replica by serving a bootstrap over an ephemeral link. pub fn bootstrap_fork(parent: &Rumors, protocol: Protocol) -> Rumors where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { pollster::block_on(async { let (mut parent_link, mut newcomer_link) = rumors::link::memory_with_capacity(CAPACITY); diff --git a/examples/swarm.rs b/examples/swarm.rs index 4cc202076..331a455b2 100644 --- a/examples/swarm.rs +++ b/examples/swarm.rs @@ -17,15 +17,15 @@ //! 3. Otherwise, run the **steady-state controller**: compare the number of //! messages it currently knows about to the target and, with a probability //! derived from that gap, either inject a fresh random message or redact a -//! key it already knows about. +//! message it already knows about. //! -//! Each party keeps its *own* `Vec` of every key it has observed — fed -//! by an [`UnorderedMessages`] observer that replays its rumor set from -//! genesis and then yields its own inserts and everything learned over the -//! wire alike — so redactions may evict messages originally published by -//! *other* parties, and the contagion spreads on the next sync. The key -//! vector is per-thread: no shared rumor-set state, no lock contention on -//! the hot path. +//! Each party keeps its *own* `Vec` of every message it has +//! observed — fed by an [`UnorderedMessages`] observer that replays its +//! rumor set from genesis and then yields its own inserts and everything +//! learned over the wire alike — so redactions may evict messages +//! originally published by *other* parties, and the contagion spreads on +//! the next sync. The version vector is per-thread: no shared rumor-set +//! state, no lock contention on the hot path. //! //! # Steady-state controller //! @@ -40,8 +40,8 @@ //! At `L = 0` it always adds; at `L = T` the odds are even; as `L` grows past //! `T` adding becomes rare. The fixed point is `L = T`, so each node's live //! count is driven toward the target, tunable live from the UI. A redact op -//! always removes a key that is still live, discarding pool entries other -//! parties already redacted as they surface; [`steady_state_op`] explains +//! always removes a message that is still live, discarding pool entries +//! other parties already redacted as they surface; [`steady_state_op`] explains //! why the fixed point depends on that, and `swarm/tests.rs` pins //! convergence through retargeting. Because redactions and inserts both //! propagate, every node's `L` tracks the global live count as views @@ -149,7 +149,7 @@ use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph, Sparkline}; use rumors::link::{Connector, Done, Link, LinkParts, MemoryAcceptor, MemoryConnector, MemoryLink}; -use rumors::{Key, Peer, Retire, Rumors, UnorderedMessages}; +use rumors::{Peer, Retire, Rumors, UnorderedMessages, Version}; use tokio::io::{AsyncRead, AsyncWrite, DuplexStream, ReadBuf}; /// Mint a genuine party-disjoint peer that inherits `parent`'s content. @@ -182,9 +182,9 @@ fn bootstrap_fork( .into_rumors() } -/// Message payload type: opaque, randomized bytes. Borsh serializes `Vec` -/// as a length-prefixed blob, so the wire cost tracks the message size -/// directly. +/// Message payload type: opaque, randomized bytes. CBOR encodes `Vec` +/// as an integer array, so the wire cost tracks the message size (within +/// a small per-element constant). type Payload = Vec; /// One endpoint of a sync session, handed from an initiator to the responder @@ -384,9 +384,9 @@ enum Command { WindDown { reply: Sender }, } -/// A party's [`Rumors`] handed back to the coordinator. The key pool is not -/// carried along: the receiving thread rebuilds it by replaying the set -/// through a fresh [`UnorderedMessages`] observer. +/// A party's [`Rumors`] handed back to the coordinator. The version pool +/// is not carried along: the receiving thread rebuilds it by replaying the +/// set through a fresh [`UnorderedMessages`] observer. struct Donation { rumors: Rumors, } @@ -417,9 +417,10 @@ fn main() -> io::Result<()> { assert!(args.parties >= 2, "need at least 2 parties to gossip"); assert!(args.message_size > 0, "message size must be positive"); - // Seed the shared rumor set, then fork one disjoint party per thread. The - // seed's keys are shared by every party, so any party may redact them - // (each thread learns them by replaying its set through an observer). + // Seed the shared rumor set, then fork one disjoint party per thread. + // The seed's messages are shared by every party, so any party may redact + // them (each thread learns them by replaying its set through an + // observer). let seed_runtime = tokio::runtime::Builder::new_current_thread() .build() .expect("build seed runtime"); @@ -527,7 +528,7 @@ const SHUTDOWN_DRAIN_DEADLINE: Duration = Duration::from_secs(5); /// `me` is this party's own directory entry, held directly so the hot path /// never has to look itself up. /// -/// The redaction key pool is fed by an [`UnorderedMessages`] observer from +/// The redaction pool is fed by an [`UnorderedMessages`] observer from /// genesis: the initial drain replays everything the party inherited (the /// seed content, or a fork parent's whole set), and each loop's drain picks /// up its own inserts and everything learned over the wire, exactly once @@ -545,7 +546,7 @@ fn run_party( let mut rng = SmallRng::from_entropy(); let mut next_sync = Instant::now() + exponential(&mut rng, &net.controls); let mut observer = rumors.unordered_messages(); - let mut keys: Vec = Vec::new(); + let mut pool: Vec = Vec::new(); loop { // 1. Serve every inbound session. Our engaged flag was set true by the @@ -555,12 +556,12 @@ fn run_party( me.engaged.store(false, Ordering::Release); } - // Catch the key pool up with everything observed since the last turn - // — the sessions just served included — and republish our live count - // for the UI gauge. One snapshot serves the rest of the iteration: - // taking it after the serves keeps the controller's odds and the - // pool's liveness checks current with what just arrived. - drain_keys(&mut observer, &mut keys); + // Catch the redaction pool up with everything observed since the + // last turn — the sessions just served included — and republish our + // live count for the UI gauge. One snapshot serves the rest of the + // iteration: taking it after the serves keeps the controller's odds + // and the pool's liveness checks current with what just arrived. + drain_versions(&mut observer, &mut pool); let snap = rumors.snapshot(); me.live.store(snap.len() as u64, Ordering::Relaxed); @@ -570,7 +571,7 @@ fn run_party( Command::Fork { reply } => { // Mint a genuine disjoint child that inherits our content, // so it can independently churn and gossip; its thread - // rebuilds the key pool by observer replay. We keep + // rebuilds the version pool by observer replay. We keep // running unchanged. let child = bootstrap_fork(&runtime, &rumors, net.duplex_capacity); let _ = reply.send(Donation { rumors: child }); @@ -609,16 +610,16 @@ fn run_party( } // 4. Local churn under the steady-state controller. - local_op(&net, &mut rng, &rumors, &snap, &mut keys); + local_op(&net, &mut rng, &rumors, &snap, &mut pool); } } /// Pull every message the observer has pending — without blocking — and push -/// its key into the pool. Each message is yielded exactly once across the -/// party's lifetime, so the pool never holds duplicates. -fn drain_keys(observer: &mut UnorderedMessages, keys: &mut Vec) { - while let Some(Some((key, _, _))) = observer.borrow_next().now_or_never() { - keys.push(key); +/// its version into the pool. Each message is yielded exactly once across +/// the party's lifetime, so the pool never holds duplicates. +fn drain_versions(observer: &mut UnorderedMessages, pool: &mut Vec) { + while let Some(Some((version, _))) = observer.borrow_next().now_or_never() { + pool.push(version.clone()); } } @@ -731,7 +732,8 @@ fn try_initiate( // Latency is the wall-clock span of the gossip exchange itself: `start` is // taken immediately before the protocol runs and `elapsed` immediately // after it returns. It is never derived from the Poisson schedule. - // (Learned keys surface through the party's observer on its next drain.) + // (Learned messages surface through the party's observer on its next + // drain.) let start = Instant::now(); // The swarm's links are in-process and its parties one universe, so a // failed session is a bug and panicking is honest. A real application @@ -751,7 +753,8 @@ fn try_initiate( } /// Drive the responder side of a session that some initiator opened with us. -/// (Learned keys surface through the party's observer on its next drain.) +/// (Learned messages surface through the party's observer on its next +/// drain.) fn serve_sync( runtime: &tokio::runtime::Runtime, net: &Net, @@ -777,26 +780,26 @@ fn local_op( rng: &mut SmallRng, rumors: &Rumors, snap: &rumors::Snapshot, - keys: &mut Vec, + pool: &mut Vec, ) { let target = net.controls.target.load(Ordering::Relaxed); let size = net.controls.message_size.load(Ordering::Relaxed) as usize; - steady_state_op(rng, rumors, snap, keys, target, size); + steady_state_op(rng, rumors, snap, pool, target, size); net.metrics.local_ops.fetch_add(1, Ordering::Relaxed); } /// One steady-state controller op: insert with probability /// `target / (target + live)` (1.0 when empty, 0.5 at target, → 0 far over), -/// otherwise redact a **live** key, so the live set is driven toward +/// otherwise redact a **live** message, so the live set is driven toward /// `target`. /// -/// Falls back to an insert when no live key is in the pool. +/// Falls back to an insert when no live message is in the pool. /// -/// The redact arm draws until it finds a key still present in `snap`, -/// discarding stale entries — keys other parties already redacted — as they -/// surface, without spending the op on them. The discard is what keeps the -/// fixed point at `live == target` for any swarm size: every party's pool -/// takes in every party's inserts but drains only by its own draws, so +/// The redact arm draws until it finds a message still present in `snap`, +/// discarding stale entries — messages other parties already redacted — as +/// they surface, without spending the op on them. The discard is what keeps +/// the fixed point at `live == target` for any swarm size: every party's +/// pool takes in every party's inserts but drains only by its own draws, so /// counting a stale draw as the op's redaction would let the stale backlog /// grow with the swarm and starve the downward pressure (live counts then /// stall near `parties × target` instead). @@ -804,7 +807,7 @@ fn steady_state_op( rng: &mut SmallRng, rumors: &Rumors, snap: &rumors::Snapshot, - keys: &mut Vec, + pool: &mut Vec, target: u64, message_size: usize, ) { @@ -817,21 +820,22 @@ fn steady_state_op( }; if !rng.gen_bool(p_add.clamp(0.0, 1.0)) { - // Swap-remove random keys until one is still live, and redact it: the - // key leaves our local view and the redaction propagates on our next - // sync. Stale keys leave the vector as they are drawn, so a redaction - // burst elsewhere costs at most one pass over the pool here. - while !keys.is_empty() { - let idx = rng.gen_range(0..keys.len()); - let key = keys.swap_remove(idx); - if snap.get(&key).is_some() { - rumors.redact(key); + // Swap-remove random pool entries until one is still live, and + // redact it: the message leaves our local view and the redaction + // propagates on our next sync. Stale entries leave the vector as + // they are drawn, so a redaction burst elsewhere costs at most one + // pass over the pool here. + while !pool.is_empty() { + let idx = rng.gen_range(0..pool.len()); + let version = pool.swap_remove(idx); + if snap.get(&version).is_some() { + rumors.redact(&version); return; } } } - // The add arm — or a pool with no live key left in it. The minted key - // reaches the pool through the observer's next drain. + // The add arm — or a pool with no live message left in it. The minted + // version reaches the pool through the observer's next drain. rumors.send(random_message(rng, message_size)); } diff --git a/examples/swarm/tests.rs b/examples/swarm/tests.rs index 30ae320cf..6104c1ebe 100644 --- a/examples/swarm/tests.rs +++ b/examples/swarm/tests.rs @@ -2,8 +2,8 @@ //! //! The controller's contract is that each node's live-message count //! converges onto the target — including after retargeting, and including -//! when other parties' redactions have strewn stale keys through the local -//! pool. Everything here is single-threaded and seeded, so a failure +//! when other parties' redactions have strewn stale entries through the +//! local pool. Everything here is single-threaded and seeded, so a failure //! reproduces exactly. use super::*; @@ -15,12 +15,12 @@ const TEST_MESSAGE_SIZE: usize = 32; /// In-memory stream capacity for the test links, matching the swarm default. const TEST_DUPLEX_CAPACITY: usize = 16 * 1024; -/// One test party: its rumor set, observer-fed key pool, and seeded rng — -/// the same per-thread state `run_party` keeps, minus the threads. +/// One test party: its rumor set, observer-fed version pool, and seeded +/// rng — the same per-thread state `run_party` keeps, minus the threads. struct Party { rumors: Rumors, observer: UnorderedMessages, - keys: Vec, + pool: Vec, rng: SmallRng, } @@ -30,7 +30,7 @@ impl Party { Party { rumors, observer, - keys: Vec::new(), + pool: Vec::new(), rng: SmallRng::seed_from_u64(seed), } } @@ -39,13 +39,13 @@ impl Party { /// and snapshotting before each op exactly as the party loop does. fn churn(&mut self, target: u64, ops: usize) { for _ in 0..ops { - drain_keys(&mut self.observer, &mut self.keys); + drain_versions(&mut self.observer, &mut self.pool); let snap = self.rumors.snapshot(); steady_state_op( &mut self.rng, &self.rumors, &snap, - &mut self.keys, + &mut self.pool, target, TEST_MESSAGE_SIZE, ); @@ -70,11 +70,11 @@ fn reconcile(runtime: &tokio::runtime::Runtime, a: &Rumors, b: &Rumors< /// /// Driving three gossiping parties through a target drop and a target raise /// must land each party's live count within half-to-double of every phase's -/// target, even though every phase's redaction bursts fill each party's key +/// target, even though every phase's redaction bursts fill each party's /// pool with entries the others already redacted. Three parties is the /// smallest swarm where those stale entries outpace the pool's drain (each /// party's draws must keep up with everyone's inserts), so a controller -/// that burns its redact ops on stale keys stalls far above a lowered +/// that burns its redact ops on stale entries stalls far above a lowered /// target here, while a two-party run would sit at the balance boundary /// and hide the defect. #[test] diff --git a/proptest-regressions/message/tests.txt b/proptest-regressions/message/tests.txt new file mode 100644 index 000000000..eef56e460 --- /dev/null +++ b/proptest-regressions/message/tests.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 382ef22c318b7e7f545f7e2587a25f6db964eb33dfda5464557129ffc8b6bf32 # shrinks to p = Payload { id: 0, tag: "", data: [] } diff --git a/proptest-regressions/tree/mirror/alternating/message/tests.txt b/proptest-regressions/tree/mirror/alternating/message/tests.txt new file mode 100644 index 000000000..0a08ee2f7 --- /dev/null +++ b/proptest-regressions/tree/mirror/alternating/message/tests.txt @@ -0,0 +1,9 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc c2ae54d74566f028417f3f6f6d5911ab17b8e31cac981b648a39ba9a0d3fc373 # shrinks to providing_entries = [([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], Node { prefix: "", children: Leaf { version: 0, message: () } })] +cc 194ca0758e26b1a80899be625e69746a733be3fccc0d3e0e05e077c019cf5643 # shrinks to providing_entries = [([], Some(Node { prefix: "32808dff4ce5ec9f4e78e36ba3500d0a7a61e32d973b81c6075dbebe2015f2f2", children: Leaf { version: (0, 0, 1), message: () } }))], requested = [], uncertain = [] +cc bc768ba70efde529acf81ed6926a73c824b48d57fad92b6c76e7f8face0a0ff8 # shrinks to providing_entries = [([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], Node { prefix: "", children: Leaf { version: 0, message: () } })], requested = [] diff --git a/proptest-regressions/tree/mirror/alternating/tests.txt b/proptest-regressions/tree/mirror/alternating/tests.txt new file mode 100644 index 000000000..3766bca8f --- /dev/null +++ b/proptest-regressions/tree/mirror/alternating/tests.txt @@ -0,0 +1,11 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 4b1aaa8eda476ea0cefac16c3df0cac0ecdd7e9e8b520fd400a7c1e47c962987 # shrinks to a = Root { ceiling: 0, root: None }, b = Root { ceiling: 0, root: None } +cc e5cc494f1fd38a845dc1a308867cffe9dd4f7683ea69528b19b464d12bc1cfe0 # shrinks to a = Root { ceiling: 0, root: None }, b = Root { ceiling: 0, root: None }, c = Root { ceiling: 0, root: None } +cc a25e553fbe4ff608fa5a1b6080e08162c4eb6dcf17de28d62a3226016e732edd # shrinks to a = Root { ceiling: 0, root: None }, b = Root { ceiling: 0, root: None } +cc d47c2614beb3a1c297850a3300795c74fc8cdab01fca246e64406c2837a76af8 # shrinks to entries_a = [], entries_b = [] +cc a630eed5224a79143c4aba2a0b643b2d17fdacca06cd453bf03f8464940ea3ca # shrinks to a = Root { ceiling: 0, root: None } diff --git a/src/batch.rs b/src/batch.rs index 4c21a3654..d56f0b7dc 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -1,10 +1,11 @@ -use borsh::BorshSerialize; use tokio::sync::watch; use crate::message::Message; use crate::tree::Action; -use crate::{Inner, Key}; +use crate::tree::typed::Path; +use crate::{Inner, Version}; +use serde::Serialize; /// A batch of insertions and redactions against a [`Rumors`](crate::Rumors), /// applied in one commit. /// @@ -13,7 +14,7 @@ use crate::{Inner, Key}; /// [`Rumors`](crate::Rumors). Dropping the batch commits it: the single-action /// case reads as a plain call (`rumors.send(message);` commits at the end of /// the statement), and chaining accumulates -/// (`rumors.batch().send(a).send(b).redact(key);`) into one commit. +/// (`rumors.batch().send(a).send(b).redact(&version);`) into one commit. /// /// # A batch is a performance optimization, not an atomicity guarantee /// @@ -62,17 +63,17 @@ impl<'a, T: Send + Sync> Batch<'a, T> { /// commit: the failure surfaces at the offending call. pub fn send(&mut self, message: T) -> &mut Self where - T: BorshSerialize, + T: Serialize, { self.actions.push(Action::Insert(Message::from(message))); self } - /// Redacts a [`Key`] as part of this batch. + /// Redacts the message stamped with `version` as part of this batch. /// - /// Redacting a key not held at commit time is a no-op. - pub fn redact(&mut self, key: Key) -> &mut Self { - self.actions.push(Action::Forget(key)); + /// Redacting a version not held at commit time is a no-op. + pub fn redact(&mut self, version: &Version) -> &mut Self { + self.actions.push(Action::Forget(Path::for_leaf(version))); self } } diff --git a/src/bookmark/format.rs b/src/bookmark/format.rs index 0db1f749d..22809a333 100644 --- a/src/bookmark/format.rs +++ b/src/bookmark/format.rs @@ -8,17 +8,17 @@ //! [ magic : 14 bytes = b"RUMORSBOOKMARK" //! | version : 2 bytes (big-endian u16, BOOKMARK_FORMAT_VERSION) //! | hash : 32 bytes BLAKE3(magic ‖ version ‖ payload) -//! | payload : N bytes borsh(BTreeMap>) ] +//! | payload : N bytes CBOR(BTreeMap>) ] //! ``` //! //! The magic and version tag reject a foreign or future file *loudly* — a //! non-bookmark or a format this build does not understand is an error, never a //! misparse. The hash covers the whole frame body, so a truncated or bit-rotted -//! file is caught before its bytes are ever borsh-decoded into a [`Clock`] — the +//! file is caught before its bytes are ever decoded into a [`Clock`] — the //! silent-divergence failure mode this crate exists to prevent. //! //! The hash is a plain [`blake3`] digest, deliberately *not* the tree's -//! content-addressing hash: that type's contract is identity (a leaf's path), a +//! path-identity hash: that type's contract is identity (a leaf's path), a //! different concern from this one's local, non-adversarial corruption check. //! //! The framing ([`frame`]/[`unframe`]) is kept separate from the record codec @@ -41,12 +41,12 @@ pub const BOOKMARK_MAGIC: [u8; 14] = *b"RUMORSBOOKMARK"; /// On-disk bookmark format version, following [`BOOKMARK_MAGIC`]. /// -/// Bumped whenever the frame layout or payload encoding changes — version 2 -/// carries the skyline version coding in its payloads. A file carrying any -/// other version (including version-1 files, which carry the packed -/// per-node coding) is rejected with [`FormatError::VersionMismatch`] +/// Bumped whenever the frame layout or payload encoding changes — version 3 +/// carries the CBOR record encoding (clocks as byte strings wrapping their +/// canonical codec) with skyline version coding inside. A file carrying +/// any other version is rejected with [`FormatError::VersionMismatch`] /// rather than misread; there is no migration path. -pub const BOOKMARK_FORMAT_VERSION: u16 = 2; +pub const BOOKMARK_FORMAT_VERSION: u16 = 3; /// Byte offset of the version field within a frame. const VERSION_OFFSET: usize = BOOKMARK_MAGIC.len(); @@ -54,7 +54,7 @@ const VERSION_OFFSET: usize = BOOKMARK_MAGIC.len(); const HASH_OFFSET: usize = VERSION_OFFSET + 2; /// Width of the BLAKE3 integrity hash, in bytes. const HASH_LEN: usize = 32; -/// Byte offset of the borsh payload within a frame: the end of the fixed header. +/// Byte offset of the payload within a frame: the end of the fixed header. const PAYLOAD_OFFSET: usize = HASH_OFFSET + HASH_LEN; /// Total fixed-header width: magic, version, and hash, before the payload. const HEADER_LEN: usize = PAYLOAD_OFFSET; @@ -175,17 +175,20 @@ pub(crate) fn unframe(bytes: &[u8]) -> Result<&[u8], FormatError> { /// Serialize a record into a complete bookmark frame. /// -/// Borsh-encodes the record, then [`frame`]s it. The inverse of [`decode`]. +/// CBOR-encodes the record, then [`frame`]s it. The inverse of [`decode`]. pub(crate) fn encode(record: &BTreeMap>) -> Vec { - // Encoding to a `Vec` cannot fail: borsh only errors on a failing writer, - // and a `Vec` never fails to extend. - let payload = borsh::to_vec(record).expect("encoding a record to a Vec is infallible"); + // Encoding to a `Vec` cannot fail: every field's serde form is a plain + // byte string or container, and a `Vec` never fails to extend. + let mut payload = Vec::new(); + ciborium::ser::into_writer(record, &mut payload) + .expect("encoding a record to a Vec is infallible"); frame(&payload) } /// Validate a bookmark frame and deserialize its record. /// -/// [`unframe`]s, then borsh-decodes the payload. The inverse of [`encode`]. +/// [`unframe`]s, then CBOR-decodes the payload, which must be exactly one +/// CBOR value. The inverse of [`encode`]. /// /// # Errors /// @@ -194,7 +197,20 @@ pub(crate) fn encode(record: &BTreeMap>) -> Vec { /// corruption). pub(crate) fn decode(bytes: &[u8]) -> Result>, FormatError> { let payload = unframe(bytes)?; - borsh::from_slice(payload).map_err(FormatError::Decode) + let mut input = payload; + let record = ciborium::de::from_reader(&mut input).map_err(|e| { + FormatError::Decode(match e { + ciborium::de::Error::Io(e) => e, + e => std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()), + }) + })?; + if !input.is_empty() { + return Err(FormatError::Decode(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("{} trailing bytes after the bookmark record", input.len()), + ))); + } + Ok(record) } #[cfg(test)] diff --git a/src/bookmark/format/snapshots/rumors__bookmark__format__tests__frame_empty.snap b/src/bookmark/format/snapshots/rumors__bookmark__format__tests__frame_empty.snap index 617ec07b8..37d312aac 100644 --- a/src/bookmark/format/snapshots/rumors__bookmark__format__tests__frame_empty.snap +++ b/src/bookmark/format/snapshots/rumors__bookmark__format__tests__frame_empty.snap @@ -2,4 +2,4 @@ source: src/bookmark/format/tests.rs expression: "hex::encode(encode(&BTreeMap::new()))" --- -52554d4f5253424f4f4b4d41524b0002da197fa6b10fa5ce79a5c79795d52253bf17e0883566dcd32884e995e4ed7e8500000000 +52554d4f5253424f4f4b4d41524b000332608c879e1aedee21ce002c988d34a3e0d21205800a036c1a1c839266777695a0 diff --git a/src/bookmark/format/snapshots/rumors__bookmark__format__tests__frame_non_trivial.snap b/src/bookmark/format/snapshots/rumors__bookmark__format__tests__frame_non_trivial.snap index e932fa5ae..f5ee05fe2 100644 --- a/src/bookmark/format/snapshots/rumors__bookmark__format__tests__frame_non_trivial.snap +++ b/src/bookmark/format/snapshots/rumors__bookmark__format__tests__frame_non_trivial.snap @@ -2,4 +2,4 @@ source: src/bookmark/format/tests.rs expression: "hex::encode(encode(&sample_record()))" --- -52554d4f5253424f4f4b4d41524b00024febc1745e9d042256252986d04805229b43b0c68620267d7c4998ce30e4af7b010000005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a03000000a22aa580482aa58092e0 +52554d4f5253424f4f4b4d41524b0003524403588d34607dc2d33e5ad6534c3a4266ca837990c8bc270311589dd50bc6a1505a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a8344a22aa58044482aa5804292e0 diff --git a/src/bookmark/format/tests.rs b/src/bookmark/format/tests.rs index 000ffeb6e..0e129ff63 100644 --- a/src/bookmark/format/tests.rs +++ b/src/bookmark/format/tests.rs @@ -32,10 +32,15 @@ fn sample_record() -> BTreeMap> { BTreeMap::from([(network, vec![clock, first, second])]) } -/// Two records are equal when their canonical borsh encodings are: a [`Clock`] +/// Two records are equal when their CBOR encodings are: a [`Clock`] /// is `!Clone` and exposes no value equality, so the bytes are the oracle. -fn borsh_eq(a: &BTreeMap>, b: &BTreeMap>) -> bool { - borsh::to_vec(a).unwrap() == borsh::to_vec(b).unwrap() +fn record_eq(a: &BTreeMap>, b: &BTreeMap>) -> bool { + let encode = |record: &BTreeMap>| { + let mut buf = Vec::new(); + ciborium::ser::into_writer(record, &mut buf).unwrap(); + buf + }; + encode(a) == encode(b) } proptest! { @@ -83,7 +88,7 @@ proptest! { let record = BTreeMap::from([(Network::from_bytes(network), clocks)]); let decoded = decode(&encode(&record)).expect("a freshly encoded record decodes"); - prop_assert!(borsh_eq(&decoded, &record)); + prop_assert!(record_eq(&decoded, &record)); } } @@ -157,7 +162,7 @@ fn short_input_is_truncated() { } /// The encoded empty record pins byte-for-byte: a header (magic, version, -/// integrity hash) over the borsh encoding of an empty map. A change here is a +/// integrity hash) over the CBOR encoding of an empty map. A change here is a /// deliberate on-disk format change, like the wire-format snapshots. #[test] fn pins_the_empty_frame() { diff --git a/src/conformance/backend.rs b/src/conformance/backend.rs index cb6a14427..6d8390b26 100644 --- a/src/conformance/backend.rs +++ b/src/conformance/backend.rs @@ -735,7 +735,7 @@ where let mut leaves: Vec<(Prefix, ChargedNode>)> = Vec::new(); for (version, payload) in messages { let message = Message::new(*payload); - let path = Path::for_leaf(version, message.bytes()); + let path = Path::for_leaf(version); let leaf = > as Leaf>::leaf(version.clone(), message) .await .expect("corpus leaves construct at rest"); diff --git a/src/conformance/link.rs b/src/conformance/link.rs index e351fde8e..2fc459cba 100644 --- a/src/conformance/link.rs +++ b/src/conformance/link.rs @@ -137,7 +137,7 @@ const CANCEL_DROP_PATIENCE: usize = 32; /// pair. /// /// Stream count follows the reconciled tree's depth, not the payload -/// count: content-addressed keys keep a corpus this size one or two levels +/// count: hashed leaf paths keep a corpus this size one or two levels /// deep, opening one or two streams per direction. The check's final /// assertion pins exactly what the sizing buys (every direction opened at /// least one data stream in-session), so it cannot rot silently; the diff --git a/src/error.rs b/src/error.rs index db522c562..76fade6fe 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,7 +8,7 @@ //! //! | Variant | Replica | Beyond reconnecting | //! |---|---|---| -//! | [`Error::Io`] | unchanged | transport failure (retry over a fresh link), or a Borsh framing fault outside the streaming mirror (counterparty bug: report it) | +//! | [`Error::Io`] | unchanged | transport failure (retry over a fresh link), or a wire framing fault outside the streaming mirror (counterparty bug: report it) | //! | [`Error::MagicMismatch`] | unchanged | the counterparty is not speaking rumors: fix the dial target | //! | [`Error::VersionMismatch`] | unchanged | select the same [`Protocol`] at both ends; if both already do, the selected protocol's wire version differs across the two releases: align crate versions | //! | [`Error::NetworkMismatch`] | unchanged | unrelated universes: apply the dominance rule ([`Peer`](crate::Peer)'s "Bootstrapping without consensus") | @@ -52,10 +52,10 @@ pub type MirrorError = mirror::Error, RemoteError< #[non_exhaustive] #[derive(Debug, thiserror::Error)] pub enum Error { - /// An underlying reader/writer error, or a Borsh framing failure outside + /// An underlying reader/writer error, or a wire framing failure outside /// the streaming mirror itself. #[error(transparent)] - Io(#[from] borsh::io::Error), + Io(#[from] std::io::Error), /// The peer's preamble did not begin with [`PROTOCOL_MAGIC`](crate::PROTOCOL_MAGIC). #[error("peer is not a rumors stream (remote magic: {remote_magic:x?})")] diff --git a/src/lib.rs b/src/lib.rs index 3cb5e37d6..ed5d45e39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -169,7 +169,7 @@ //! //! // 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 @@ -181,9 +181,9 @@ //! # 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. @@ -230,19 +230,17 @@ //! [`AsyncRead`](tokio::io::AsyncRead) and [`AsyncWrite`](tokio::io::AsyncWrite); //! no Tokio runtime, spawning, sockets, or timers are required by this crate. //! -//! # Wire compatibility +//! # Message payloads and compatibility //! -//! Every session opens with a fixed 25-byte preamble carrying -//! [`PROTOCOL_MAGIC`], the selected [`Protocol`]'s version, the network, and -//! session intent. -//! A counterparty that is not speaking `rumors`, or speaks an incompatible -//! version, is rejected before any peer-declared frame length is trusted -//! ([`Error::MagicMismatch`], [`Error::VersionMismatch`]). -//! [`Protocol::V2`] is the default; `Protocol::V1` (behind the `protocol-v1` -//! cargo feature) can be selected on an established [`Peer`] with -//! [`Peer::protocol`], or while joining with -//! [`Bootstrap::protocol`]. Both endpoints must select the same -//! protocol. +//! Your message type `T` needs [`serde::Serialize`] and +//! [`serde::de::DeserializeOwned`]; payloads are serialized 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 break compatibility with prior versions +//! of your type `T`; however, *renaming breaks compabitility*. It is worth +//! designing around this from the get-go: consider an outer `enum` indicating +//! the version of your application-level message type, even if it starts +//! out only having one variant, `V1`. //! //! # Cargo features //! @@ -251,20 +249,16 @@ //! - `conformance`: the public validation suite for caller-built [`link`] //! instantiations (the [`conformance::link`] module). Enable it from a //! dev-dependency; it is safe, though pointless, in an application. -//! - `protocol-v1`: the strictly alternating `Protocol::V1`, kept for wire -//! compatibility with V1 peers and comparative measurement. Enabling it -//! compiles a large per-height state-machine surface into the binary, -//! which is why it is off by default. +//! - `protocol-v1`: the strictly alternating `Protocol::V1`, kept for +//! comparative measurement. //! - `test-internals`: this crate's own test scaffolding, enabled through -//! its self-referential dev-dependency. Never enable it in an -//! application. +//! its self-referential dev-dependency. Never enable it in an application. //! //! # Stability and testing //! //! The wire format is steady by design: each [`Protocol`] is pinned //! byte-for-byte by snapshot tests, and once a version has shipped, a wire -//! change introduces a new protocol version rather than mutating a released -//! one. +//! change introduces a new protocol version. //! //! The crate is validated by property tests stating the model's invariants //! (convergence under arbitrary gossip schedules, deletion honoring, observer @@ -304,7 +298,6 @@ mod tests; pub use crate::peer::PROTOCOL_MAGIC; pub use ::before; -pub use ::borsh; pub use batch::Batch; pub use before::{Ticks, Version, causally}; pub use bookmark::{ @@ -322,6 +315,5 @@ pub use peer::{ pub use protocol::Protocol; pub use rumors::{CausalMessages, Changes, Rumors, TryNext, TryTick, UnorderedMessages}; pub use snapshot::Snapshot; -pub use tree::Key; pub use tree::MERKLE_HASH_LEN; pub use tree::mirror::streaming::stats::SessionStats; diff --git a/src/message.rs b/src/message.rs index 79b502793..dd8779615 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,21 +1,40 @@ use std::cmp::Ordering; use std::fmt; use std::hash::{Hash, Hasher}; +use std::io; use std::sync::Arc; -use borsh::{BorshDeserialize, BorshSerialize}; use bytes::Bytes; +use serde::Deserialize; +use serde::Deserializer; +use serde::Serialize; +use serde::Serializer; +use serde::de::DeserializeOwned; /// A message of type `T` paired with its cached serialization. /// /// The cache avoids repeated roundtrips through serialization: a `Message` -/// always serializes identically to a `T`. Cloning is cheap, because the -/// serialized bytes are shared and the message is enclosed in an `Arc`. +/// always carries the exact CBOR bytes its `T` was encoded to or decoded +/// from. Cloning is cheap, because the serialized bytes are shared and the +/// message is enclosed in an `Arc`. +/// +/// The payload encoding is CBOR (via [`ciborium`]): self-describing, so +/// field and variant *names* are the wire contract — a decoder pairs fields +/// by name, tolerating reordering — and no canonical encoding is required +/// of `T`, because payload bytes carry no identity (a leaf's identity is +/// its version). /// /// # Panics /// -/// All messages of type `T` are assumed serializable; methods that attempt -/// serialization panic if serialization fails. +/// Every value of `T` must serialize: methods that serialize (`new`, +/// `from_arc`, `From`) panic if `T`'s [`serde::Serialize`] +/// implementation reports an error. Encoding runs into an in-memory +/// buffer and CBOR imposes no format-driven failures (any map key, any +/// nesting), so the only trigger is the implementation itself declining a +/// value — which this crate treats as a bug in `T`, exactly as `Ord`'s +/// totality is trusted. Types whose `Serialize` is data-dependently +/// fallible (for example `std::path::PathBuf`, which errors on non-UTF-8 +/// paths) violate that obligation and must not be used as message types. pub struct Message { message: Arc, serialized: Bytes, @@ -30,45 +49,104 @@ impl Clone for Message { } } +/// Map a ciborium deserialization failure into `io::Error`, keeping the +/// truncation/corruption split callers classify by: a reader's own error +/// passes through, everything else is invalid data. +fn de_error(error: ciborium::de::Error) -> io::Error { + match error { + ciborium::de::Error::Io(error) => error, + error => io::Error::new(io::ErrorKind::InvalidData, error.to_string()), + } +} + +/// Encode one value as CBOR into a fresh buffer. +/// +/// # Panics +/// +/// If `T`'s `Serialize` implementation reports an error ([`Message`]'s +/// panic contract: serializability is the caller's obligation). Writing +/// into a `Vec` cannot fail. +fn to_vec(value: &T) -> Vec { + let mut buf = Vec::new(); + ciborium::ser::into_writer(value, &mut buf) + .expect("every message value must serialize (see Message's panic contract)"); + buf +} + impl Message { /// Creates a `Message` pairing the given object with its cached /// serialization. /// /// # Panics /// - /// If the message cannot be serialized. + /// If the message cannot be serialized (see [`Message`]). pub fn new(message: T) -> Self where - T: BorshSerialize, + T: Serialize, { Message { - serialized: Bytes::from(borsh::to_vec(&message).unwrap()), + serialized: Bytes::from(to_vec(&message)), message: Arc::new(message), } } /// Creates a `Message` pairing the given serialized bytes with the /// object derived by deserializing them. - pub fn from_slice(bytes: &[u8]) -> borsh::io::Result + /// + /// The bytes must be exactly one CBOR value: trailing bytes are + /// rejected as invalid data, so the cache is always the value's exact + /// encoding. + pub fn from_slice(bytes: &[u8]) -> io::Result where - T: BorshDeserialize, + T: DeserializeOwned, { + let mut input = bytes; + let message: T = ciborium::de::from_reader(&mut input).map_err(de_error)?; + if !input.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("{} trailing bytes after the message payload", input.len()), + )); + } Ok(Message { - message: Arc::new(borsh::from_slice(bytes)?), + message: Arc::new(message), serialized: Bytes::copy_from_slice(bytes), }) } + /// Pairs an already-decoded object with the exact bytes it was decoded + /// from. + /// + /// The caller certifies the pairing: `serialized` must be exactly the + /// CBOR encoding `message` was parsed out of (the wire codec's record + /// parser upholds this — it hands over precisely the bytes its parse + /// consumed). + pub(crate) fn from_decoded(message: T, serialized: Bytes) -> Self { + Message { + message: Arc::new(message), + serialized, + } + } + /// Creates a `Message` from already-shared serialized bytes, without /// copying. /// - /// The bytes are deserialized to produce the paired object. - pub fn from_bytes(bytes: Bytes) -> borsh::io::Result + /// The bytes are deserialized to produce the paired object, under + /// [`from_slice`](Self::from_slice)'s exactly-one-value contract. + pub fn from_bytes(bytes: Bytes) -> io::Result where - T: BorshDeserialize, + T: DeserializeOwned, { + let mut input = bytes.as_ref(); + let message: T = ciborium::de::from_reader(&mut input).map_err(de_error)?; + if !input.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("{} trailing bytes after the message payload", input.len()), + )); + } Ok(Message { - message: Arc::new(borsh::from_slice(bytes.as_ref())?), + message: Arc::new(message), serialized: bytes, }) } @@ -77,13 +155,13 @@ impl Message { /// /// # Panics /// - /// If the message cannot be serialized. + /// If the message cannot be serialized (see [`Message`]). pub fn from_arc(arc: Arc) -> Self where - T: BorshSerialize, + T: Serialize, { Message { - serialized: Bytes::from(borsh::to_vec(&*arc).unwrap()), + serialized: Bytes::from(to_vec(&*arc)), message: arc, } } @@ -139,13 +217,13 @@ impl Message { } } -impl From for Message { +impl From for Message { /// Creates a `Message` pairing the given object with its cached /// serialization. /// /// # Panics /// - /// If the message cannot be serialized. + /// If the message cannot be serialized (see [`Message`]). fn from(message: T) -> Self { Self::new(message) } @@ -200,44 +278,21 @@ impl Hash for Message { } } -// Borsh impls let `Message` nest inside other borsh types with the same -// on-the-wire representation as `T` itself. +// The serde form lets `Message` nest inside larger CBOR values without +// re-encoding: one byte string wrapping the cached CBOR payload. The +// wrapper is what makes a nested message self-delimiting wherever the +// container does not delimit it. -impl BorshSerialize for Message { - fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { - // Write the cached bytes directly: the whole point of `Message` is - // to avoid reserializing. - writer.write_all(&self.serialized) +impl Serialize for Message { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_bytes(&self.serialized) } } -impl BorshDeserialize for Message { - fn deserialize_reader(reader: &mut R) -> borsh::io::Result { - // Tee the reader so we capture exactly the bytes consumed while - // parsing `T`, and use them as the cached serialization. - let mut captured = Vec::new(); - let mut tee = TeeReader { - inner: reader, - buf: &mut captured, - }; - let message = Arc::new(T::deserialize_reader(&mut tee)?); - Ok(Message { - message, - serialized: captured.into(), - }) - } -} - -struct TeeReader<'a, R: ?Sized> { - inner: &'a mut R, - buf: &'a mut Vec, -} - -impl borsh::io::Read for TeeReader<'_, R> { - fn read(&mut self, out: &mut [u8]) -> borsh::io::Result { - let n = self.inner.read(out)?; - self.buf.extend_from_slice(&out[..n]); - Ok(n) +impl<'de, T: DeserializeOwned> Deserialize<'de> for Message { + fn deserialize>(deserializer: D) -> Result { + let bytes = >::deserialize(deserializer)?; + Message::from_slice(&bytes).map_err(serde::de::Error::custom) } } diff --git a/src/message/tests.rs b/src/message/tests.rs index a82de84d2..5d8451960 100644 --- a/src/message/tests.rs +++ b/src/message/tests.rs @@ -1,16 +1,17 @@ use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; -use borsh::{BorshDeserialize, BorshSerialize}; use bytes::Bytes; use proptest::prelude::*; +use serde::{Deserialize, Serialize}; use super::Message; -/// A small borsh-serializable payload with varied field types, so proptests -/// exercise nontrivial serialization structure (length prefixes, nested -/// vectors) rather than only fixed-width primitives. -#[derive(Clone, Debug, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize)] +use serde::Serializer; +/// A small serde payload with varied field types, so proptests exercise +/// nontrivial serialization structure (nested containers, strings) rather +/// than only fixed-width primitives. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] struct Payload { id: u64, tag: String, @@ -31,13 +32,20 @@ fn hash_of(value: &T) -> u64 { h.finish() } +/// Encode a value as one CBOR value, as `Message::new` does internally. +fn cbor_vec(value: &T) -> Vec { + let mut buf = Vec::new(); + ciborium::ser::into_writer(value, &mut buf).unwrap(); + buf +} + proptest! { /// After construction via `new`, the cached serialized bytes are exactly - /// what borsh would produce for the inner value. + /// the value's CBOR encoding. #[test] - fn new_caches_borsh_serialization(p in payload()) { + fn new_caches_cbor_serialization(p in payload()) { let m = Message::new(p.clone()); - let direct = borsh::to_vec(&p).unwrap(); + let direct = cbor_vec(&p); prop_assert_eq!(m.bytes(), direct.as_slice()); prop_assert_eq!(m.message(), &p); } @@ -46,7 +54,7 @@ proptest! { /// bytes in the cache, with no reserialization drift. #[test] fn from_slice_roundtrips(p in payload()) { - let bytes = borsh::to_vec(&p).unwrap(); + let bytes = cbor_vec(&p); let m = Message::::from_slice(&bytes).unwrap(); prop_assert_eq!(m.message(), &p); prop_assert_eq!(m.bytes(), bytes.as_slice()); @@ -56,63 +64,83 @@ proptest! { /// `Message`s from the same input. #[test] fn from_bytes_matches_from_slice(p in payload()) { - let bytes = borsh::to_vec(&p).unwrap(); + let bytes = cbor_vec(&p); let a = Message::::from_slice(&bytes).unwrap(); let b = Message::::from_bytes(Bytes::from(bytes.clone())).unwrap(); prop_assert_eq!(&a, &b); prop_assert_eq!(a.bytes(), b.bytes()); } - /// `BorshSerialize` on a `Message` writes exactly the cached bytes, so - /// serializing a `Message` is indistinguishable from serializing `T`. + /// A payload followed by trailing bytes is rejected: the cache is + /// always exactly one CBOR value's encoding, never a value plus noise. #[test] - fn serialize_writes_cached_bytes(p in payload()) { - let m = Message::new(p.clone()); - let reserialized = borsh::to_vec(&m).unwrap(); - prop_assert_eq!(reserialized.as_slice(), m.bytes()); - prop_assert_eq!(reserialized, borsh::to_vec(&p).unwrap()); + fn trailing_bytes_are_rejected(p in payload(), trailer in proptest::collection::vec(any::(), 1..8)) { + let mut bytes = cbor_vec(&p); + bytes.extend_from_slice(&trailer); + prop_assert!(Message::::from_slice(&bytes).is_err()); + prop_assert!(Message::::from_bytes(Bytes::from(bytes)).is_err()); } - /// A `Message` roundtrips through borsh: deserializing a serialized - /// message yields an equal message with equal cached bytes. + /// The serde form of a `Message` is one CBOR byte string wrapping + /// the cached payload bytes — never a re-encoding of `T` — so nesting + /// a message in a larger CBOR value costs one length header. #[test] - fn borsh_roundtrip(p in payload()) { + fn serde_form_wraps_cached_bytes(p in payload()) { + struct Bstr<'a>(&'a [u8]); + impl Serialize for Bstr<'_> { + fn serialize(&self, s: S) -> Result { + s.serialize_bytes(self.0) + } + } let m = Message::new(p); - let bytes = borsh::to_vec(&m).unwrap(); - let back: Message = borsh::from_slice(&bytes).unwrap(); - prop_assert_eq!(&m, &back); - prop_assert_eq!(m.bytes(), back.bytes()); + let wrapped = cbor_vec(&m); + let direct = cbor_vec(&Bstr(m.bytes())); + prop_assert_eq!(wrapped, direct); } - /// `BorshDeserialize` captures only the bytes actually consumed by `T`: - /// when a `Message` is embedded alongside trailing data, the cached - /// bytes match `T`'s serialization and the trailing data survives. + /// A `Message` roundtrips through its serde form: deserializing a + /// serialized message yields an equal message with equal cached bytes. #[test] - fn deserialize_captures_only_message_bytes(p in payload(), trailer in any::>()) { - let expected = borsh::to_vec(&p).unwrap(); - let mut combined = expected.clone(); - combined.extend_from_slice(&trailer); - - let mut slice: &[u8] = &combined; - let m = Message::::deserialize_reader(&mut slice).unwrap(); - prop_assert_eq!(m.bytes(), expected.as_slice()); - prop_assert_eq!(slice, trailer.as_slice()); + fn serde_roundtrip(p in payload()) { + let m = Message::new(p); + let bytes = cbor_vec(&m); + let back: Message = ciborium::de::from_reader(bytes.as_slice()).unwrap(); + prop_assert_eq!(&m, &back); + prop_assert_eq!(m.bytes(), back.bytes()); } - /// `Message` nests correctly inside other borsh types: a `Vec>` - /// roundtrips and preserves each element's cached bytes. + /// `Message` nests correctly inside other CBOR containers: a + /// `Vec>` roundtrips and preserves each element's cached + /// bytes. #[test] fn nested_in_vec_roundtrips(ps in proptest::collection::vec(payload(), 0..8)) { let msgs: Vec> = ps.into_iter().map(Message::new).collect(); - let bytes = borsh::to_vec(&msgs).unwrap(); - let back: Vec> = borsh::from_slice(&bytes).unwrap(); + let bytes = cbor_vec(&msgs); + let back: Vec> = ciborium::de::from_reader(bytes.as_slice()).unwrap(); prop_assert_eq!(&msgs, &back); for (a, b) in msgs.iter().zip(back.iter()) { prop_assert_eq!(a.bytes(), b.bytes()); } } + /// Reading a message off a stream consumes exactly the message's own + /// bytes: trailing data after the CBOR value survives for the next + /// field (the property the wire codec's mid-stream decodes rest on). + #[test] + fn stream_decode_consumes_only_message_bytes(p in payload(), trailer in any::>()) { + let m = Message::new(p); + let mut combined = cbor_vec(&m); + let expected = combined.clone(); + combined.extend_from_slice(&trailer); + + let mut slice: &[u8] = &combined; + let back: Message = ciborium::de::from_reader(&mut slice).unwrap(); + prop_assert_eq!(back.bytes(), m.bytes()); + prop_assert_eq!(slice, trailer.as_slice()); + prop_assert_eq!(combined.len() - slice.len(), expected.len()); + } + /// Equal `Message` values hash identically, so `Hash` agrees with /// `PartialEq` as required by the standard library contract. #[test] diff --git a/src/network.rs b/src/network.rs index d42a75588..dfa592ea0 100644 --- a/src/network.rs +++ b/src/network.rs @@ -2,9 +2,12 @@ use std::fmt; -use borsh::{BorshDeserialize, BorshSerialize}; use rand::RngCore; +use serde::Deserialize; +use serde::Deserializer; +use serde::Serialize; +use serde::Serializer; /// The identifier shared by every [`Rumors`](crate::Rumors) that descends from /// the same [`seed`](crate::Peer::seed). /// @@ -18,9 +21,30 @@ use rand::RngCore; /// This type is opaque and [`Copy`]: callers can read it off a `Peer` with /// [`network`](crate::Peer::network) and compare two for equality, but cannot /// create one except through [`seed`](crate::Peer::seed). -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, BorshDeserialize, BorshSerialize)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Network([u8; 16]); +// The serde form is one byte string of the 16 raw bytes: the identifier is +// opaque, so no structure beyond its width belongs on the wire or on disk +// (the bookmark record keys its map by it). + +impl Serialize for Network { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_bytes(&self.0) + } +} + +impl<'de> Deserialize<'de> for Network { + fn deserialize>(deserializer: D) -> Result { + let bytes = >::deserialize(deserializer)?; + let bytes: [u8; 16] = bytes + .as_slice() + .try_into() + .map_err(|_| serde::de::Error::custom("a network identifier is exactly 16 bytes"))?; + Ok(Network(bytes)) + } +} + impl Network { /// The all-zero placeholder a bootstrapping peer sends in the handshake: it /// has no rumor set yet, hence no real network. diff --git a/src/peer.rs b/src/peer.rs index 81b7bb6ea..ed0e2b2ab 100644 --- a/src/peer.rs +++ b/src/peer.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use before::Party; -use borsh::{BorshDeserialize, BorshSerialize}; use rand::{RngCore, rngs::OsRng}; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::sync::{Mutex, watch}; @@ -18,10 +17,12 @@ use crate::tree::mirror::streaming::remote::RunBudget; pub use crate::tree::mirror::streaming::window::DEFAULT_SYNC_MEMORY_BUDGET; use crate::tree::mirror::streaming::window::WindowConfig; use crate::{ - Batch, Bookmark, CausalMessages, Key, Network, Protocol, Rumors, Snapshot, UnorderedMessages, + Batch, Bookmark, CausalMessages, Network, Protocol, Rumors, Snapshot, UnorderedMessages, Version, }; +use serde::Serialize; +use serde::de::DeserializeOwned; mod bootstrap; mod gossip; @@ -274,7 +275,7 @@ impl Peer { /// promises](crate::link::Link#what-a-session-promises). pub async fn retire(self, link: &mut Link) -> Retire where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -336,7 +337,7 @@ impl Peer { /// capacities from what the two replicas exchange at session start: /// exact set sizes and version-size bounds, so every input to the /// worst case is on the table before the descent begins. Under - /// uniform content hashing, dispute populations thin geometrically + /// uniform version hashing, dispute populations thin geometrically /// with depth and scale with the *product* of the two set sizes, so /// the budget buys width only where disputes can exist. The setting /// is not wire-visible: peers with different budgets interoperate. @@ -382,13 +383,13 @@ impl Peer { /// measure it. Worked figures below use the specification BDP of /// 12.5 MB, where 1 Gbps × 100 ms and 100 Gbps × 1 ms coincide; /// substitute your own measurement. Your corpus contributes the - /// other: `m`, the mean encoded record size (the borsh-encoded + /// other: `m`, the mean encoded record size (the CBOR-encoded /// payload of a disputed message's leaf record). Two constants /// then convert between bytes and disputes, both derived and /// pinned: each in-flight dispute (one disputed subtree, the unit /// the table below counts as a disputed scope) charges the budget /// a 5431 B envelope (recomputed exactly by test), and each disputed - /// message costs 34 B of wire overhead on top of its record + /// message costs 35 B of wire overhead on top of its record /// (calibrated by deterministic byte counts, /// `tests/dispute_wire.rs`). /// @@ -396,10 +397,10 @@ impl Peer { /// trade. A session's worst-case slowdown, relative to a session /// limited only by wire time, is about /// - /// > `slowdown ≈ max(1, BDP × 5431 / (budget × (34 + m)))` + /// > `slowdown ≈ max(1, BDP × 5431 / (budget × (35 + m)))` /// /// Read it as a ratio of two message counts: how many disputed - /// messages the wire holds, `BDP / (34 + m)`, against how many the + /// messages the wire holds, `BDP / (35 + m)`, against how many the /// budget keeps in flight, `budget / 5431`. Slowdown 1 is /// wire-time-optimal: bandwidth-bound stays bandwidth-bound. /// @@ -418,27 +419,28 @@ impl Peer { /// The ballpark answers, at the specification BDP: /// /// - **Is the default enough?** For any corpus whose mean encoded - /// record size is at least 61 B, yes: the default imposes no + /// record size is at least 60 B, yes: the default imposes no /// window-induced serialization at all, because the in-flight /// disputes' own transfer time covers the round trip. That - /// 61 B crossover comes from the exact solve, evaluated + /// 60 B crossover comes from the exact solve, evaluated /// self-consistently (each record size at its own BDP-scale /// corpus: the specification BDP in `m`-sized records, per side) /// and pinned by `default_crossover_matches_the_solve`; - /// the closed form's safe-side estimate is ~92 B. + /// the closed form's safe-side estimate is ~91 B. /// - **What budget removes the wait entirely?** About - /// `BDP × 5431 / (34 + m)` bytes. The design record (`m = 172`) + /// `BDP × 5431 / (35 + m)` bytes. The design record (`m = 172`) /// needs ~330 MB, where the solve agrees with the form to three /// digits (this is the design point the envelope is pinned at). - /// A minimal 8-byte-record corpus needs ~1.6 GB by the form, - /// ~1.08 GB by the solve: population caps thin the deep charge - /// at BDP-scale corpora, so the estimate is conservative there. + /// A minimal `u64`-record corpus (9 B encoded) needs ~1.5 GB by + /// the form, ~1.1 GB by the solve: population caps thin the deep + /// charge at BDP-scale corpora, so the estimate is conservative + /// there. /// - **What does a smaller budget cost?** Smooth latency, never /// memory, and only on the interleaved dispute walk (bulk supply /// runs stream outside the window). `u64` records at the default - /// run at ~4.6× wire time for a BDP-scale corpus, and the factor + /// run at ~4.3× wire time for a BDP-scale corpus, and the factor /// grows slowly with set size as the derived window narrows: - /// ~14.2× at 10⁷ messages, ~26.5× at 10¹⁰ (all derived from the + /// ~13.6× at 10⁷ messages, ~25.3× at 10¹⁰ (all derived from the /// solve). `tests/window_operator.rs` holds the wave model /// against measured sessions on a bandwidth-limited link. /// @@ -450,7 +452,7 @@ impl Peer { /// session of 62500-message corpora a side; larger corpora derive /// narrower windows. Each cell then applies the measured wave form /// `slowdown = max(1, BDP_messages / K)`, with - /// `BDP_messages = BDP / (34 + m)` evaluated at the specification + /// `BDP_messages = BDP / (35 + m)` evaluated at the specification /// BDP of 12.5 MB (the wave form is measured: /// `tests/window_knee.rs`, `tests/window_operator.rs`). One /// caution when reading it: in rows whose window reaches the @@ -542,19 +544,19 @@ impl Peer { pub(crate) fn send(&self, message: T) -> Batch<'_, T> where - T: BorshSerialize + Send + Sync, + T: Serialize + Send + Sync, { let mut batch = self.batch(); batch.send(message); batch } - pub(crate) fn redact(&self, key: Key) -> Batch<'_, T> + pub(crate) fn redact(&self, version: &Version) -> Batch<'_, T> where T: Send + Sync, { let mut batch = self.batch(); - batch.redact(key); + batch.redact(version); batch } diff --git a/src/peer/bootstrap.rs b/src/peer/bootstrap.rs index 7d00d55cf..46f33746c 100644 --- a/src/peer/bootstrap.rs +++ b/src/peer/bootstrap.rs @@ -4,7 +4,6 @@ use std::marker::PhantomData; -use borsh::{BorshDeserialize, BorshSerialize}; use tokio::io::{AsyncRead, AsyncWrite}; use crate::bookmark::{Bookmark, BookmarkError}; @@ -15,6 +14,8 @@ use crate::{Error, Peer, Protocol}; use super::gossip::Unbookmarked; +use serde::Serialize; +use serde::de::DeserializeOwned; /// Configuration for joining an existing universe: the builder behind /// [`Peer::bootstrap`]. /// @@ -216,7 +217,7 @@ impl Bootstrap { link: &mut Link, ) -> Result>, Error> where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -296,7 +297,7 @@ impl BookmarkedBootstrap { /// in every outcome that never used it. pub async fn join(self, link: &mut Link) -> Joined where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, diff --git a/src/peer/gossip.rs b/src/peer/gossip.rs index 4e362a6d6..6ec7c3c98 100644 --- a/src/peer/gossip.rs +++ b/src/peer/gossip.rs @@ -9,7 +9,6 @@ use std::pin::Pin; use std::sync::Arc; use before::Party; -use borsh::{BorshDeserialize, BorshSerialize}; use futures::{Stream, future::BoxFuture}; use futures_util::StreamExt; use tokio::{ @@ -42,6 +41,8 @@ use crate::{ use super::{Inner, Peer, bootstrap::Bootstrap}; +use serde::Serialize; +use serde::de::DeserializeOwned; /// Magic bytes that open every `rumors` gossip session's preamble frame. pub const PROTOCOL_MAGIC: [u8; 6] = *b"RUMORS"; @@ -210,7 +211,7 @@ impl Peer { link: &'a mut Link, ) -> BoxFuture<'a, Result, Error>> where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -237,7 +238,7 @@ impl Peer { link: DynLinkParts<'a>, ) -> BoxFuture<'a, Result, Error>> where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, { Box::pin(async move { let (read, write, connector, acceptor, epoch) = link; @@ -436,7 +437,7 @@ impl Peer { link: &mut Link, ) -> Retire where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -475,7 +476,7 @@ impl Peer { link: &mut Link, ) -> Result> where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -615,7 +616,7 @@ impl Peer { link: DynLinkParts<'a>, ) -> (Intent, Result<(Version, SessionStats), Error>) where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, { let (read, write, connector, acceptor, epoch) = link; // The session's stats recorder: under V2, both protocol @@ -963,7 +964,7 @@ impl Peer { link: &'a mut Link, ) -> impl Stream>> + Unpin + 'a where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, diff --git a/src/peer/gossip/tests.rs b/src/peer/gossip/tests.rs index f2a90d2c0..9d9b6dc08 100644 --- a/src/peer/gossip/tests.rs +++ b/src/peer/gossip/tests.rs @@ -151,11 +151,15 @@ fn redacted_history_root(events: u64) -> tree::Root { batch.send(v); } } - let keys: Vec<_> = donor.snapshot().iter().map(|(key, _, _)| key).collect(); + let versions: Vec<_> = donor + .snapshot() + .iter() + .map(|(version, _)| version.clone()) + .collect(); { let mut batch = donor.batch(); - for key in keys { - batch.redact(key); + for version in &versions { + batch.redact(version); } } let snapshot = donor.snapshot(); @@ -237,7 +241,7 @@ async fn claim_bootstrap_v1( Ok((party, Tree { root })) } -/// A provider holding `values`, plus its pre-session content hash. +/// A provider holding `values`, plus its pre-session root hash. fn provider_with(values: &[u64]) -> Peer { let provider = Peer::::seed(); { diff --git a/src/reconciliation.rs b/src/reconciliation.rs index 6d7c1577a..c9d6040e7 100644 --- a/src/reconciliation.rs +++ b/src/reconciliation.rs @@ -9,29 +9,39 @@ //! //! # One hash binds identity, causality, and placement //! -//! Every message is stored under a [`Key`](crate::Key): the BLAKE3 hash -//! binding the [`Version`](crate::Version) at which the message was sent to -//! the message's canonical [`borsh`] encoding. Both inputs are -//! deliberate. -//! -//! - **The version makes every send unique.** A replica's version advances -//! on every send, so each send mints a key that no other send in the -//! universe's history can mint again. Sending byte-identical content -//! twice creates two messages under two keys; redacting one never touches -//! the other; re-sending redacted content is a new message under a new -//! key, neither resurrected nor suppressed by the redaction that came -//! before it. -//! - **The content makes the address canonical.** A content address is only -//! an address if one value has exactly one encoding. Borsh guarantees -//! that by construction; serialization frameworks in general do not. -//! -//! The 32-byte key is also the message's *location*: keys are the paths of -//! a 256-ary radix trie, one key byte per level, 32 levels deep, with -//! single-child runs compressed away. Hashing spreads keys uniformly, and -//! the trie's shape is a pure function of its membership: two replicas -//! holding the same set of messages hold the *same tree*, whatever order -//! they learned it in. Each interior node memoizes two summaries of its -//! subtree: a digest (a 24-byte truncation of BLAKE3; +//! Every message is stored at one address: the BLAKE3 hash of the +//! [`Version`](crate::Version) stamped on it at send time. Nothing else +//! enters the address; it rests on the invariant the protocol already +//! requires everywhere — no two sends ever share a version (a replica's +//! version advances on every send, and disjoint parties can never mint the +//! same one). Two consequences are deliberate. +//! +//! - **Every send is a distinct message.** Sending byte-identical content +//! twice mints two versions, hence two leaves; redacting one never +//! touches the other, and re-sending redacted content is a new message, +//! neither resurrected nor suppressed by the redaction that came before +//! it. +//! - **Message bytes enter no address and no digest.** The payload +//! encoding needs no canonical form — one value may have many valid +//! encodings without splitting identity — and no author of content can +//! steer where anything lands, or what any digest reads, by choosing +//! bytes. What that buys is stated under +//! [Twenty-four-byte digests](#twenty-four-byte-digests). +//! +//! Version reuse — the only way two messages could claim one address — +//! cannot arise: every send mints a fresh version (a tick strictly above +//! everything the replica has ever held), and the linearity of parties +//! keeps replicas' versions disjoint. Producing a reused version at all +//! requires violating the linearity invariant the crate docs' safety +//! rules state, a regime that is already fatal to causal gossip. +//! +//! The 32-byte address is also the message's *location*: addresses are +//! the paths of a 256-ary radix trie, one byte per level, 32 levels deep, +//! with single-child runs compressed away. Hashing spreads paths +//! uniformly, and the trie's shape is a pure function of its membership: +//! two replicas holding the same set of messages hold the *same tree*, +//! whatever order they learned it in. Each interior node memoizes two +//! summaries of its subtree: a digest (a 24-byte truncation of BLAKE3; //! [`MERKLE_HASH_LEN`](crate::MERKLE_HASH_LEN)) and the ceiling and floor //! of its leaves' versions. The digest answers "do we hold the same things //! here?"; the version bounds answer "could anything here be news to a @@ -60,10 +70,10 @@ //! Without redaction, reconciliation would end there: ship each exclusive //! subtree's messages to the side that lacks them (*supplies*), splice, and //! both replicas hold the union. The work is proportional to the difference, -//! not to the holdings: uniform keys thin disputes geometrically with depth, +//! not to the holdings: uniform paths thin disputes geometrically with depth, //! so the disputed paths of two replicas differing in `D` of `N` total //! messages separate in about `log₂₅₆(2·D·N)` levels (in expectation, -//! derived from uniform content addressing) — about five levels for two +//! derived from uniform version hashing) — about five levels for two //! fully divergent million-message replicas, three or four when a small //! divergence sits in a large set. Round trips are governed by that depth, //! not by `D`: every dispute at a level travels concurrently (the wire @@ -97,7 +107,7 @@ //! as well as every send: redaction is itself causal, so a redacter's version //! contains the version of the since-redacted sent message, and every replica //! that catches up to it inherits that containment. (A further and more -//! technical soundness note: this also only works because keys content-address +//! technical soundness note: this also only works because addresses name //! causally-unique versions, which means that there are no A-B-A problems in //! play.) The two versions exchanged in the greeting are sufficient causal //! context to locally filter every subtree on the disjoint frontier: this means @@ -131,7 +141,7 @@ //! is designed to be truncated: any prefix is itself a cryptographic hash). //! Digest bytes dominate every dispute listing on the wire — they are the //! protocol's main metadata price — so the width is spent deliberately; -//! keys into the tree remain full 32-byte hashes. +//! leaf addresses in the tree remain full 32-byte hashes. //! //! The width prices a specific, severe failure. A false-equal — two //! differing subtrees whose digests read equal at the same prefix — is not @@ -143,21 +153,27 @@ //! holder deletes it. A landed false-equal permanently deletes the //! divergent messages fleet-wide. //! -//! The acceptance is priced in-model, on the accident bound alone: a digest -//! at prefix `P` is only ever compared against the counterparty's digest at -//! the same `P`, so a false-equal is a per-interior-comparison event at -//! 2⁻¹⁹² — pairwise, never birthday-amplified across the tree's population — -//! and at that bound it does not occur by accident at any realistic session +//! The acceptance is priced on the accident bound: a digest at prefix `P` +//! is only ever compared against the counterparty's digest at the same +//! `P`, so a false-equal is a per-interior-comparison event at 2⁻¹⁹² — +//! pairwise, never birthday-amplified across the tree's population — and +//! at that bound it does not occur by accident at any realistic session //! volume. //! -//! Off-model note: peers are trusted in this crate's model (see [when -//! *shouldn't* you use it](crate#when-shouldnt-you-use-it)), so hostile-peer -//! regimes are out of scope and nothing above rests on what an attack would -//! cost. For the one adjacent actor the model does admit — an author of -//! message *content* who is not a peer — the 24-byte width puts the offline -//! birthday floor for grinding any colliding content pair at 2⁹⁶ hash -//! evaluations, closing that vector unconditionally rather than -//! economically. +//! Every compared digest is a pure function of the *version set*: a leaf's +//! digest commits its address and its version, a branch's commits its +//! children, and message bytes appear nowhere. An author of message +//! content therefore contributes zero bits to any compared quantity — the +//! offline content-grinding route to a collision is structurally gone, not +//! merely priced. What could still contribute bits is influence over which +//! versions get minted (an actor steering gossip schedules steers the +//! version set); against any such actor, the 24-byte width keeps the +//! offline birthday floor at 2⁹⁶ evaluations, an unconditional bound that +//! rests on no premise about capabilities. Hostile *peers* remain +//! off-model entirely (see [when *shouldn't* you use +//! it](crate#when-shouldnt-you-use-it)): peers hold write authority +//! already, so no width buys anything against a member and none is priced +//! here. //! //! # The bytes on the wire //! diff --git a/src/rumors.rs b/src/rumors.rs index 3a1c71c29..a2a4fabea 100644 --- a/src/rumors.rs +++ b/src/rumors.rs @@ -8,8 +8,7 @@ pub use unordered::{TryNext, UnorderedMessages}; use crate::bookmark::{Bookmark, BookmarkError, NoBookmark}; use crate::link::{Acceptor, Connector, Link}; -use crate::{Batch, Error, Gossiped, Key, Network, Peer, Snapshot, Version}; -use borsh::{BorshDeserialize, BorshSerialize}; +use crate::{Batch, Error, Gossiped, Network, Peer, Snapshot, Version}; use futures::Stream; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -18,6 +17,8 @@ use tokio::{ sync::watch, }; +use serde::Serialize; +use serde::de::DeserializeOwned; /// A handle for [`send`](Rumors::send)ing and [`redact`](Rumors::redact)ing /// messages, and [`gossip`](Rumors::gossip)ing the result with peers. /// @@ -139,13 +140,12 @@ impl Rumors { /// cancellation commits its queued prefix, so never hold one across an /// `.await` in a cancellable task ([`Batch`] states the drop semantics). /// - /// `send` does not return the message's [`Key`]. Keys come back through - /// observation: the observers and [`Snapshot`] attach every message to - /// its key, and every send gets a key unique across the universe's - /// whole history (a key binds the send's fresh version to the content, - /// so even byte-identical re-sends are distinct messages under distinct - /// keys). [`redact`](Self::redact) states the intended - /// observe-then-redact pattern and why the write path carries no key. + /// `send` does not return the message's [`Version`]. Versions come back + /// through observation: the observers and [`Snapshot`] attach every + /// message to the version its send minted, unique across the universe's + /// whole history, so even byte-identical re-sends are distinct messages + /// under distinct versions. [`redact`](Self::redact) states the intended + /// observe-then-redact pattern and why the write path returns nothing. /// /// # Observe-then-send is domination /// @@ -162,17 +162,17 @@ impl Rumors { /// If `message` fails to serialize (see [`Batch::send`]). pub fn send(&self, message: T) -> Batch<'_, T> where - T: BorshSerialize + Send + Sync, + T: Serialize + Send + Sync, { self.peer.send(message) } - /// Redact a message: remove the live message named by `key` from the set, - /// here and, through gossip, everywhere. Redacting a key not currently - /// held is a no-op. + /// Redact a message: remove the live message stamped with `version` from + /// the set, here and, through gossip, everywhere. Redacting a version not + /// currently held is a no-op. /// /// Returns a [`Batch`] that commits when dropped: a bare - /// `rumors.redact(key);` commits at the end of the statement, and chaining + /// `rumors.redact(&version);` commits at the end of the statement, and chaining /// further [`send`](Batch::send)s and [`redact`](Batch::redact)s /// accumulates them into one commit. A batch dropped by async /// cancellation commits its queued prefix, so never hold one across an @@ -186,28 +186,29 @@ impl Rumors { /// deletions from the causal frontiers the two sides exchange. A /// message the counterparty's version shows it must already have seen, /// yet it no longer holds, was deleted there, so the holder drops its - /// own copy instead of transmitting it. And because a [`Key`] binds - /// the send's fresh version to the content, re-sending byte-identical - /// content after a redaction is a *new* message: no resurrection, no - /// suppression. For the same reason, two identical sends are two - /// messages, and redacting one never touches the other. - /// - /// # Where the key comes from - /// - /// [`send`](Self::send) does not return a [`Key`], deliberately, for - /// two reasons. The intended shape of an application is a state machine - /// driven from observed messages: the observers and [`Snapshot`] - /// attach every message to its `Key`, so the read path, not the write - /// path, is where a key-holding workflow like send-then-redact lives. - /// Observe your own message back out, keep its key, redact it later. - /// And batching breaks the correspondence anyway: a batch inserts all - /// its messages at once, so sends are not 1:1 with insertions and a - /// message's `Key` is not knowable until insertion. - pub fn redact(&self, key: Key) -> Batch<'_, T> + /// own copy instead of transmitting it. And because every send mints a + /// fresh version, re-sending byte-identical content after a redaction + /// is a *new* message: no resurrection, no suppression. For the same + /// reason, two identical sends are two messages, and redacting one + /// never touches the other. + /// + /// # Where the version comes from + /// + /// [`send`](Self::send) does not return a [`Version`], deliberately, + /// for two reasons. The intended shape of an application is a state + /// machine driven from observed messages: the observers and + /// [`Snapshot`] attach every message to its version, so the read path, + /// not the write path, is where a version-holding workflow like + /// send-then-redact lives. Observe your own message back out, keep its + /// version, redact it later. And batching breaks the correspondence + /// anyway: a batch inserts all its messages at once, so sends are not + /// 1:1 with insertions and a message's version is not knowable until + /// insertion. + pub fn redact(&self, version: &Version) -> Batch<'_, T> where T: Send + Sync, { - self.peer.redact(key) + self.peer.redact(version) } /// Start an empty [`Batch`], for applying several changes in one @@ -392,7 +393,7 @@ impl Rumors { link: &mut Link, ) -> Result> where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -508,7 +509,7 @@ impl Rumors { link: &'a mut Link, ) -> impl Stream>> + Unpin + 'a where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, diff --git a/src/rumors/causal.rs b/src/rumors/causal.rs index bcc1be38e..caac9c337 100644 --- a/src/rumors/causal.rs +++ b/src/rumors/causal.rs @@ -8,7 +8,7 @@ use futures::Stream; use tokio::sync::watch; use crate::tree::Leaf; -use crate::{Key, Version, causally}; +use crate::{Version, causally}; use super::unordered::{Channel, TryNext}; @@ -46,13 +46,19 @@ pub struct CausalMessages { /// staged, undelivered message nor the delivered message still in the /// caller's hands. checkpoint: Version, - /// The undelivered backlog, in causal-rank order. Always the residue of - /// a *single* ingest (a new pass opens only once this empties), whose - /// range start was `checkpoint` and whose ceiling is `ingested`. - staged: BTreeMap<(Rank, Key), Leaf>, + /// The undelivered backlog in rank-then-canonical-bytes order. + /// + /// The same total order as [`before::Ranked`], with the [`Rank`] + /// materialized once per leaf so repeated map comparisons stay cheap. + /// Rank extends the causal order and the byte tiebreak fires only + /// between concurrent messages, so delivery order is causal and + /// deterministic. Always the residue of a *single* ingest (a new pass + /// opens only once this empties), whose range start was `checkpoint` + /// and whose ceiling is `ingested`. + staged: BTreeMap<(Rank, Vec), Leaf>, /// The most recently delivered leaf, kept alive so its version and /// value can be lent to the caller until the next call. - current: Option<(Key, Leaf)>, + current: Option>, } impl CausalMessages { @@ -76,7 +82,7 @@ impl CausalMessages { /// complete. The watch read guard lives only long enough to freeze the /// walk and capture the ceiling; the walk itself runs unlocked. fn ingest( - staged: &mut BTreeMap<(Rank, Key), Leaf>, + staged: &mut BTreeMap<(Rank, Vec), Leaf>, ingested: &mut Version, rx: &mut watch::Receiver>, ) where @@ -89,8 +95,9 @@ impl CausalMessages { inner.tree.latest().clone(), ) }; - while let Some((key, leaf)) = walk.next() { - staged.insert((leaf.version().rank(), key), leaf); + while let Some((_, leaf)) = walk.next() { + let version = leaf.version(); + staged.insert((version.rank(), version.as_bytes().to_vec()), leaf); } *ingested |= &ceiling; } @@ -103,14 +110,14 @@ impl CausalMessages { /// catch-up is deferred to the next call, exactly as /// [`UnorderedMessages`](super::UnorderedMessages) defers a drained /// pass's ceiling. - fn pop(&mut self) -> Option<(Key, &Version, &Arc)> { - let ((_, key), leaf) = self.staged.pop_first()?; - let (key, leaf) = self.current.insert((key, leaf)); - Some((*key, leaf.version(), leaf.value())) + fn pop(&mut self) -> Option<(&Version, &Arc)> { + let (_, leaf) = self.staged.pop_first()?; + let leaf = self.current.insert(leaf); + Some((leaf.version(), leaf.value())) } /// Advance to the next message in causal order and lend it. - pub(crate) async fn borrow_next_inner(&mut self) -> Option<(Key, &Version, &Arc)> + pub(crate) async fn borrow_next_inner(&mut self) -> Option<(&Version, &Arc)> where T: Send + Sync, { @@ -183,7 +190,7 @@ impl CausalMessages { /// /// Awaits quietly while the set is unchanged; resolves [`None`] once no /// further change is possible and the backlog has drained. - pub async fn borrow_next(&mut self) -> Option<(Key, &Version, &Arc)> + pub async fn borrow_next(&mut self) -> Option<(&Version, &Arc)> where T: Send + Sync, { @@ -207,14 +214,14 @@ impl CausalMessages { } } -/// The owned-item face: `(Key, Version, Arc)` per item, popped from the +/// The owned-item face: `(Version, Arc)` per item, popped from the /// same staged backlog [`borrow_next`](CausalMessages::borrow_next) lends /// from. /// /// `T: 'static` because the quiet-period wait is materialized as an /// owned future, exactly as in [`UnorderedMessages`](super::UnorderedMessages). impl Stream for CausalMessages { - type Item = (Key, Version, Arc); + type Item = (Version, Arc); fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); @@ -223,8 +230,8 @@ impl Stream for CausalMessages { // empties the backlog: the yielded message is unhandled until // the stream is polled again, so the catch-up defers to the // next poll's ingest. - if let Some(((_, key), leaf)) = this.staged.pop_first() { - return Poll::Ready(Some((key, leaf.version().clone(), leaf.value().clone()))); + if let Some((_, leaf)) = this.staged.pop_first() { + return Poll::Ready(Some((leaf.version().clone(), leaf.value().clone()))); } match this.channel.as_mut().expect("channel state present") { Channel::Waiting(wait) => match wait.as_mut().poll(cx) { diff --git a/src/rumors/unordered.rs b/src/rumors/unordered.rs index 4934294ab..e67b54a57 100644 --- a/src/rumors/unordered.rs +++ b/src/rumors/unordered.rs @@ -1,5 +1,5 @@ use crate::tree::{Leaf, RangeOwned}; -use crate::{Key, Version, causally}; +use crate::{Version, causally}; use futures::Stream; use std::pin::Pin; use std::sync::Arc; @@ -18,10 +18,10 @@ use tokio::sync::watch; /// /// There are two ways to use it: /// -/// - [`borrow_next`](Self::borrow_next) lends each message as `(Key, -/// &Version, &Arc)`, the borrows living until the next call. -/// - The [`Stream`] impl (for `T: 'static`) yields owned `(Key, Version, -/// Arc)`. +/// - [`borrow_next`](Self::borrow_next) lends each message as +/// `(&Version, &Arc)`, the borrows living until the next call. +/// - The [`Stream`] impl (for `T: 'static`) yields owned +/// `(Version, Arc)`. /// /// Order is unspecified and does *not* follow the causal order: a message may /// be yielded before another that causally precedes it; use @@ -44,7 +44,7 @@ pub struct UnorderedMessages { pass: Option>, /// The most recently yielded leaf, kept alive so its version and value /// can be lent to the caller until the next call. - current: Option<(Key, Leaf)>, + current: Option>, } /// The outcome of [`UnorderedMessages::try_next`] or [`CausalMessages::try_next`]. @@ -56,7 +56,7 @@ pub struct UnorderedMessages { pub enum TryNext<'a, T> { /// A message was ready, lent until the next call (as /// [`borrow_next`](UnorderedMessages::borrow_next) lends it). - Message((Key, &'a Version, &'a Arc)), + Message((&'a Version, &'a Arc)), /// No message is ready yet, but handles are still live: ask again later. Quiet, /// Every handle is gone and no further message is possible. @@ -115,7 +115,7 @@ impl UnorderedMessages { } /// Advance to the next message and lend it until the following call. - pub(crate) async fn borrow_next_inner(&mut self) -> Option<(Key, &Version, &Arc)> + pub(crate) async fn borrow_next_inner(&mut self) -> Option<(&Version, &Arc)> where T: Send + Sync, { @@ -135,9 +135,9 @@ impl UnorderedMessages { // Lend the next leaf out of the walk, parking it in // `current` so the borrows survive the return. let pass = self.pass.as_mut().expect("opened above"); - if let Some((key, leaf)) = pass.walk.next() { - let (key, leaf) = self.current.insert((key, leaf)); - return Some((*key, leaf.version(), leaf.value())); + if let Some((_, leaf)) = pass.walk.next() { + let leaf = self.current.insert(leaf); + return Some((leaf.version(), leaf.value())); } // The pass drained: absorb its ceiling as completed, @@ -185,7 +185,7 @@ impl UnorderedMessages { /// rumors.send("one".to_string()); /// /// let mut observer = rumors.unordered_messages(); - /// let (_key, _version, m) = observer.borrow_next().await.expect("one message"); + /// let (_version, m) = observer.borrow_next().await.expect("one message"); /// assert_eq!(m.as_str(), "one"); /// /// // Mid-pass, the checkpoint has not moved: resuming here would @@ -201,7 +201,7 @@ impl UnorderedMessages { /// // everything not yet delivered. /// rumors.send("two".to_string()); /// let mut resumed = rumors.unordered_messages_since(checkpoint); - /// let (_key, _version, m) = resumed.borrow_next().await.expect("only the new message"); + /// let (_version, m) = resumed.borrow_next().await.expect("only the new message"); /// assert_eq!(m.as_str(), "two"); /// # }); /// ``` @@ -214,7 +214,7 @@ impl UnorderedMessages { /// Advance to the next message, lending its version and value until the /// following call. Awaits quietly while the set is unchanged; resolves /// [`None`] once no further change is possible. - pub async fn borrow_next(&mut self) -> Option<(Key, &Version, &Arc)> + pub async fn borrow_next(&mut self) -> Option<(&Version, &Arc)> where T: Send + Sync, { @@ -238,13 +238,13 @@ impl UnorderedMessages { } } -/// The owned-item face: `(Key, Version, Arc)` per item, cloned out of +/// The owned-item face: `(Version, Arc)` per item, cloned out of /// the same engine [`borrow_next`](UnorderedMessages::borrow_next) lends from. /// /// `T: 'static` because the quiet-period wait is materialized as an owned /// future. impl Stream for UnorderedMessages { - type Item = (Key, Version, Arc); + type Item = (Version, Arc); fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); @@ -263,12 +263,8 @@ impl Stream for UnorderedMessages { Self::open_pass(&mut this.pass, rx, &this.checkpoint); let pass = this.pass.as_mut().expect("opened above"); - if let Some((key, leaf)) = pass.walk.next() { - return Poll::Ready(Some(( - key, - leaf.version().clone(), - leaf.value().clone(), - ))); + if let Some((_, leaf)) = pass.walk.next() { + return Poll::Ready(Some((leaf.version().clone(), leaf.value().clone()))); } // The pass drained: absorb its ceiling, then enter the diff --git a/src/snapshot.rs b/src/snapshot.rs index 3763c3ef3..2f72bcd37 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -1,8 +1,8 @@ -use crate::{Key, Network, Version, causally, tree::Tree}; +use crate::{Network, Version, causally, tree::Tree}; use std::sync::Arc; /// The iterator of [`Snapshot::iter`], re-exported from the tree internals: -/// every live message as `(Key, &Version, &Arc)`, unspecified order, +/// every live message as `(&Version, &Arc)`, unspecified order, /// exact-size and double-ended. pub use crate::tree::Iter; @@ -81,12 +81,14 @@ impl Snapshot { self.tree.hash() } - /// Looks up a single live message by its [`Key`]. - pub fn get(&self, key: &Key) -> Option<(&Version, &Arc)> { - self.tree.get(key) + /// Looks up the live message stamped with `version` (for example, a + /// version an observer yielded earlier). Returns `None` when no live + /// message carries it — never sent here, or since redacted. + pub fn get(&self, version: &Version) -> Option<(&Version, &Arc)> { + self.tree.get(version) } - /// Iterates every live message as `(Key, &Version, &Arc)`. + /// Iterates every live message as `(&Version, &Arc)`. /// /// Order is unspecified, and in particular does *not* follow the causal /// order: a message may be yielded before another that causally precedes @@ -94,7 +96,7 @@ impl Snapshot { /// ordering consistent with causality. pub fn iter( &self, - ) -> impl DoubleEndedIterator)> + ExactSizeIterator + Send + Sync + ) -> impl DoubleEndedIterator)> + ExactSizeIterator + Send + Sync where T: Send + Sync, { @@ -143,7 +145,7 @@ impl Snapshot { pub fn range<'q, P: causally::Polarity>( &'q self, query: impl Into>, - ) -> impl DoubleEndedIterator)> + Send + Sync + ) -> impl DoubleEndedIterator)> + Send + Sync where T: Send + Sync, { @@ -160,7 +162,7 @@ impl Snapshot { } impl<'a, T: Send + Sync> IntoIterator for &'a Snapshot { - type Item = (Key, &'a Version, &'a Arc); + type Item = (&'a Version, &'a Arc); type IntoIter = Iter<'a, T>; fn into_iter(self) -> Self::IntoIter { diff --git a/src/testing.rs b/src/testing.rs index 5444629f1..048724d7a 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -209,10 +209,11 @@ pub fn window_tradeoff_table() -> String { ("2 GiB", 2 << 30), ]; - /// The mean-encoded-record-size columns: the minimal `u64` record, - /// a mid value, the design record, and a fat value. + /// The mean-encoded-record-size columns: the minimal `u64` record + /// (9 B in CBOR: header plus value), a mid value, the design record, + /// and a fat value. const RECORD_SIZES: &[(usize, &str)] = &[ - (8, "m = 8 (u64)"), + (9, "m = 9 (u64)"), (64, "m = 64"), (DESIGN_RECORD_BYTES, "m = 172 (design record)"), (1024, "m = 1024"), diff --git a/src/tests.rs b/src/tests.rs index 9e6d33ceb..1ba9f5ac2 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -270,21 +270,22 @@ fn greeting_frame_len(retiree: &Peer) -> usize { let root: streaming::Root = retiree.inner.borrow().tree.clone().root.into(); let fan = pollster::block_on(materialized::greeting_fan(&Local, root.root)) .unwrap_or_else(|never| match never {}); - let listing = borsh::to_vec(&materialized::fan_listing(&fan)).expect("a listing serializes"); + // The listing frame is raw radix-hash records: one byte plus a Merkle + // hash per child, the frame length carrying the count. + let listing_len = + materialized::fan_listing(&fan).len() * (1 + crate::tree::typed::hash::MERKLE_HASH_LEN); crate::tree::mirror::framing::LENGTH_HEADER_LEN + crate::tree::mirror::framing::GREETING_SIZE_WORDS_LEN + retiree.snapshot().latest().as_bytes().len() + crate::tree::mirror::framing::LENGTH_HEADER_LEN - + listing.len() + + listing_len } /// The wire length of `retiree`'s trailing party frame, so a [`Fuse`] budget /// can land on an exact protocol boundary. fn party_frame_len(retiree: &Peer) -> usize { - crate::tree::mirror::framing::LENGTH_HEADER_LEN - + borsh::to_vec(&party_of(retiree)) - .expect("a party serializes") - .len() + // The party frame's body is the canonical party encoding, bare. + crate::tree::mirror::framing::LENGTH_HEADER_LEN + party_of(retiree).as_bytes().len() } /// A connector whose opened streams draw on the link's shared fuse budget. diff --git a/src/tree.rs b/src/tree.rs index 210b3ae28..4dacb3349 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -7,19 +7,22 @@ //! # Shape //! //! Branching factor 256, fixed depth 32: a leaf's path is its 32-byte -//! content address, one byte per level, derived from the hash of its -//! `(version, value)` pair ([`Path::for_leaf`](typed::Path::for_leaf)). -//! Content addressing buys three properties at once: +//! version address, one byte per level — the full-width hash of the +//! leaf's version ([`Path::for_leaf`](typed::Path::for_leaf)). Message +//! bytes enter no path and no digest; identity rests on the invariant the +//! protocol already requires everywhere, that no two messages ever share +//! a version. Version addressing buys three properties at once: //! //! - **The set is the tree.** Where a leaf lives is fully determined by -//! what it is, so two replicas holding the same messages hold the same -//! tree, regardless of insertion order or which peer sent what. Union is -//! well-defined node-by-node. +//! the version stamped on it, so two replicas holding the same messages +//! hold the same tree, regardless of insertion order or which peer sent +//! what. Union is well-defined node-by-node. //! - **Equal hash ⟹ equal subtree.** Each node memoizes a Merkle hash of -//! its subtree, so replicas can prune agreement wholesale — the engine of +//! its subtree — a pure function of the version set, blind to message +//! bytes — so replicas can prune agreement wholesale: the engine of //! the [`mirror`] protocol's divergence-proportional cost. The Merkle //! hash is a 24-byte truncation, deliberately narrower than the 32-byte -//! content address: a comparison signal tolerates truncation that an +//! version address: a comparison signal tolerates truncation that an //! identity cannot (see [`typed::Hash`] for the asymmetry argument). //! - **Uniform spread.** Hashed paths are uniform, so the trie is //! expected-balanced with no adversarial input shape; depth bounds are @@ -59,13 +62,15 @@ use std::sync::Arc; -mod key; -mod traverse; +pub(crate) mod traverse; pub(crate) mod typed; +// The alternating protocol is the only production consumer; the typed +// tree's own tests exercise the codec regardless of features. +#[cfg(any(test, feature = "protocol-v1"))] +pub(crate) mod wire; use crate::{Version, causally, message::Message, tree::typed::Node}; -pub use key::Key; pub use typed::hash::MERKLE_HASH_LEN; pub mod mirror; @@ -79,11 +84,12 @@ pub use typed::{Leaf, RangeOwned}; /// leaves store versioned [`Message`]s. /// /// The tree has a branching factor of 256 and a depth of 32, so a leaf's -/// 32-byte path is its content-addressed hash (see -/// [`Path::for_leaf`](typed::Path::for_leaf)). The version is folded into -/// the path, so two content-identical messages inserted at distinct -/// versions occupy distinct leaves; two leaves collide only when they carry -/// the same `(version, value)` pair, which disjoint parties cannot produce. +/// 32-byte path is the full-width hash of its version (see +/// [`Path::for_leaf`](typed::Path::for_leaf)). Versions are unique per +/// send — locally by tick, globally by party disjointness — so two +/// content-identical messages sent at distinct moments occupy distinct +/// leaves, and two leaves collide only when a version has been reused, +/// which conforming peers cannot do. #[derive(Debug, Eq)] pub struct Tree { pub(crate) root: Root, @@ -159,23 +165,23 @@ impl Default for Tree { pub enum Action { /// Insert some value, tagged at the current version by your own party. Insert(Message), - /// Forget the value corresponding to a hash. - Forget(Key), + /// Forget the leaf at a version-derived path. + Forget(typed::Path), } /// The iterator of [`Snapshot::iter`](crate::Snapshot::iter): /// a lazy depth-first walk over every live message as -/// `(Key, &Version, &Arc)`, in unspecified order. +/// `(&Version, &Arc)`, in unspecified order. /// /// An [`ExactSizeIterator`] (the live-message count is known up front) and a /// [`DoubleEndedIterator`]. pub struct Iter<'a, T>(typed::Iter<'a, T>); impl<'a, T> Iterator for Iter<'a, T> { - type Item = (Key, &'a Version, &'a Arc); + type Item = (&'a Version, &'a Arc); fn next(&mut self) -> Option { - self.0.next().map(|(k, v, m)| (k, v, m.as_arc())) + self.0.next().map(|(v, m)| (v, m.as_arc())) } fn size_hint(&self) -> (usize, Option) { @@ -185,7 +191,7 @@ impl<'a, T> Iterator for Iter<'a, T> { impl<'a, T> DoubleEndedIterator for Iter<'a, T> { fn next_back(&mut self) -> Option { - self.0.next_back().map(|(k, v, m)| (k, v, m.as_arc())) + self.0.next_back().map(|(v, m)| (v, m.as_arc())) } } @@ -269,12 +275,14 @@ impl Tree { Node::root_hash(&self.root.clone().into()).into() } - /// Looks up a single live message by its [`Key`]. - pub fn get(&self, key: &Key) -> Option<(&Version, &Arc)> { + /// Looks up the live message stamped with `version`, by its + /// version-derived path. + pub fn get(&self, version: &Version) -> Option<(&Version, &Arc)> { + let path = <[u8; 32]>::from(typed::Path::for_leaf(version)); self.root .root .as_ref()? - .get(&key.0) + .get(&path) .map(|(version, message)| (version, message.as_arc())) } @@ -297,7 +305,7 @@ impl Tree { } /// Lazily iterates every live leaf currently in the tree as - /// `(Key, &Version, &Arc)`, in unspecified order. + /// `(&Version, &Arc)`, in unspecified order. pub fn iter(&self) -> Iter<'_, T> where T: Send + Sync, @@ -339,7 +347,7 @@ impl Tree { pub fn range<'q, P: causally::Polarity>( &'q self, query: impl Into>, - ) -> impl DoubleEndedIterator)> + Send + Sync + ) -> impl DoubleEndedIterator)> + Send + Sync where T: Send + Sync, { @@ -347,7 +355,7 @@ impl Tree { // The shared walk yields the full `&Message`; the public // contract hands out only the `&Arc` value, a cheap projection // of it. - .map(|(k, v, m)| (k, v, m.as_arc())) + .map(|(v, m)| (v, m.as_arc())) } /// Applies the specified actions as a batch to the tree, advancing its @@ -356,11 +364,11 @@ impl Tree { /// Each [`Action::Insert`] advances the local party's component of the /// version vector by one before the leaf's path is derived; the inserts /// in a batch are therefore assigned strictly-increasing versions in the - /// order they appear, and two content-identical messages within a batch - /// receive distinct keys. An [`Action::Forget`] ticks too, so an + /// order they appear, so two content-identical messages within a batch + /// occupy distinct leaves. An [`Action::Forget`] ticks too, so an /// effectual forget carries a version strictly greater than any prior /// insert (the mirror protocol's deletion-honoring inference depends on - /// that; see the body comment). A forget that targets a key derived from + /// that; see the body comment). A forget that targets the version of /// an earlier insert in the same batch overrides that insert (last /// action on a path wins). /// @@ -395,14 +403,15 @@ impl Tree { /// *above* the ceiling; session ingestion rejects the shape) can /// produce `true` without a hash change, and then the cost is one /// spurious watch wakeup, never a missed one. + /// pub fn act(&mut self, party: &before::Party, actions: I) -> bool where T: Send + Sync, I: IntoIterator>, { // Track the running version across the batch, ticking the owning party - // once per action so that (a) content-identical messages produce - // distinct keys even when submitted together, and (b) forgets carry a + // once per action so that (a) content-identical messages occupy + // distinct leaves even when submitted together, and (b) forgets carry a // version strictly greater than any prior insert at this party. The // strict tick on forgets is required by the mirror protocol's // deletion-honoring inference, which cannot distinguish "forgot it" @@ -421,17 +430,14 @@ impl Tree { let version = new_version.clone(); // Convert unversioned, unlocalized actions into reactions - // independent of our party and current version. The key is + // independent of our party and current version. The path is // derived from the post-tick version, which is unique per // insert (see [`typed::Path::for_leaf`]). - let (key, value) = match action { - Action::Forget(hash) => (hash, None), - Action::Insert(value) => { - let key = typed::Path::for_leaf(&version, value.bytes()).into(); - (key, Some(value)) - } + let (path, value) = match action { + Action::Forget(path) => (path, None), + Action::Insert(value) => (typed::Path::for_leaf(&version), Some(value)), }; - (key, version, value) + (path, version, value) })) } @@ -443,9 +449,9 @@ impl Tree { /// /// If multiple actions refer to the same leaf of the tree, the causally /// latest action wins, with order of specification breaking concurrency - /// and version ties. Each item is keyed by its version and content hash, - /// so if each party only manipulates its own tree through - /// [`Tree::act`], these conflicts cannot arise. + /// and version ties. Each item is keyed by its version-derived path, so + /// if each party only manipulates its own tree through [`Tree::act`], + /// these conflicts cannot arise. /// /// As with [`act`](Self::act), a batch is applied in a single traversal, /// which is more efficient than applying its actions one at a time but @@ -454,12 +460,14 @@ impl Tree { /// Returns whether the effectual-action observer fired at all — the /// changed flag [`act`](Self::act) hands out, with the contract stated /// there. `false` means no observation and therefore no ceiling - /// movement either: the tree is untouched. + /// movement either: the tree is untouched. Panics exactly as + /// [`traverse::act`](fn@traverse::act) does (version reuse: a crate bug, never an input), + /// with the tree untouched on unwind. fn react(&mut self, reactions: I) -> bool where T: Send + Sync, M: Into>>, - I: IntoIterator, + I: IntoIterator, { // Materialize the caller's action stream before the commit section // begins: a panicking caller iterator (`act`'s version ticks and key @@ -471,13 +479,9 @@ impl Tree { // the radix sort immediately consumes. let actions: Vec<_> = reactions .into_iter() - .map(|(key, version, message)| match message.into() { - None => (typed::Path::from(key), version, traverse::Action::Forget), - Some(value) => ( - typed::Path::from(key), - version, - traverse::Action::Insert(value), - ), + .map(|(path, version, message)| match message.into() { + None => (path, version, traverse::Action::Forget), + Some(value) => (path, version, traverse::Action::Insert(value)), }) .collect(); @@ -548,6 +552,7 @@ impl Tree { /// whose every message we already hold or honor as deleted): the flag /// answers for what observers of the *set* can see, and a ceiling-only /// join leaves the set untouched. + /// pub fn join(&mut self, other: Tree) -> bool where T: Send + Sync, @@ -584,9 +589,9 @@ impl Tree { let new_ceiling = &self.root.ceiling | their_version; // The commit point: the walk and the ceiling fold both completed - // without unwinding. Both fields are assigned before the pre-image - // drops, because that drop runs user code — everything deletion - // honoring removed from our side becomes uniquely held here, so its + // without unwinding. Both fields are assigned before the pre-image drops, + // because that drop runs user code — everything deletion honoring + // removed from our side becomes uniquely held here, so its // cascading `T` destructors run now, and a panicking destructor // must find the tree already consistent. The defense is nothing // subtler than statement order: replace, assign, then drop. diff --git a/src/tree/arb.rs b/src/tree/arb.rs index f6b247578..0d15b0d19 100644 --- a/src/tree/arb.rs +++ b/src/tree/arb.rs @@ -64,7 +64,7 @@ pub fn arb_root_node( .prop_map(move |draws| { // Tick this tree's party once per leaf, so the leaves carry a // strictly-increasing chain of versions on a single party. Each - // leaf is placed at its content-addressed path, exactly as a real + // leaf is placed at its version-derived path, exactly as a real // insert does (see [`Path::for_leaf`] and `Tree::act`): a tree with // a leaf anywhere else can never arise in production, so gossiping // one would test an impossible state. @@ -75,7 +75,7 @@ pub fn arb_root_node( .map(|()| { version.tick(&p); let message = Message::new(()); - let path = Path::for_leaf(&version, message.bytes()); + let path = Path::for_leaf(&version); (path, version.clone(), Action::Insert(message)) }) .collect(); @@ -152,7 +152,7 @@ pub fn arb_divergent_pair() -> BoxedStrategy<(crate::tree::Root<()>, crate::tree &p_s, (0..n_shared).map(|_| Action::Insert(Message::new(()))), ); - let shared_keys: Vec<_> = base.iter().map(|(k, _, _)| k).collect(); + let shared_keys: Vec<_> = base.iter().map(|(v, _)| Path::for_leaf(v)).collect(); let side = |party: &Party, n: usize, redact: &[bool]| { let mut t = base.clone(); @@ -183,7 +183,7 @@ pub fn arb_divergent_pair() -> BoxedStrategy<(crate::tree::Root<()>, crate::tree /// both. /// /// This strategy closes the proxy tier's generator gap on *budget* only, -/// deliberately not on *bias*: content addressing makes each child's radix a +/// deliberately not on *bias*: version hashing makes each child's radix a /// function of leaf /// hashes, so steering generation toward the early-radix-order deep-dispute /// shape would mean a per-case search inside the strategy. The geometry pin @@ -210,7 +210,7 @@ pub fn arb_wide_divergent_pair() -> BoxedStrategy<(crate::tree::Root<()>, crate: &p_s, (0..n_shared).map(|_| Action::Insert(Message::new(()))), ); - let shared_keys: Vec<_> = base.iter().map(|(k, _, _)| k).collect(); + let shared_keys: Vec<_> = base.iter().map(|(v, _)| Path::for_leaf(v)).collect(); let side = |party: &Party, n: usize, redact: &[bool]| { let mut t = base.clone(); @@ -340,7 +340,7 @@ pub fn early_first_child_dispute_pair() -> (crate::tree::Root<()>, crate::tree:: (0..ticks) .map(|_| { version.tick(party); - let path: [u8; 32] = Path::for_leaf(&version, Message::new(()).as_slice()).into(); + let path: [u8; 32] = Path::for_leaf(&version).into(); path[0] }) .collect() @@ -395,7 +395,10 @@ pub fn early_first_child_dispute_pair() -> (crate::tree::Root<()>, crate::tree:: // Both sides' geometry was judged from the simulation, so both // sides must agree with the honestly built trees. for (tree, firsts) in [(&left, left_firsts), (&right, right_firsts)] { - let mut built: Vec = tree.iter().map(|(k, _, _)| k.as_bytes()[0]).collect(); + let mut built: Vec = tree + .iter() + .map(|(v, _)| <[u8; 32]>::from(Path::for_leaf(v))[0]) + .collect(); let mut simulated = firsts.to_vec(); built.sort_unstable(); simulated.sort_unstable(); @@ -420,7 +423,7 @@ pub fn early_first_child_dispute_pair() -> (crate::tree::Root<()>, crate::tree:: /// the declared versions within a test's horizon — the session ceiling the /// receiver adopts, or the receiver's own later redact ticks — ever /// contains it. Returns the two roots plus the escaped leaf's -/// content-addressed path and its version. +/// version-derived path and its version. pub fn uncontained_supply_pair() -> (crate::tree::Root<()>, crate::tree::Root<()>, Path, Version) { /// How far the escaped version outruns both declared ceilings, per /// party: an upper bound on the honest ticks a test performs after @@ -458,7 +461,7 @@ pub fn uncontained_supply_pair() -> (crate::tree::Root<()>, crate::tree::Root<() // The receiving side's honest content: one leaf on its own party, // ceiling covering it, exactly as `Tree::act` would leave it. let receiver_message = Message::new(()); - let receiver_path = Path::for_leaf(&receiver_version, receiver_message.bytes()); + let receiver_path = Path::for_leaf(&receiver_version); let receiver = root_with_ceiling( act( None, @@ -485,7 +488,7 @@ pub fn uncontained_supply_pair() -> (crate::tree::Root<()>, crate::tree::Root<() ); let message = Message::new(()); - let path = Path::for_leaf(&escaped, message.bytes()); + let path = Path::for_leaf(&escaped); let poisoned = root_with_ceiling( act( None, @@ -500,7 +503,7 @@ pub fn uncontained_supply_pair() -> (crate::tree::Root<()>, crate::tree::Root<() /// A path all-zero except its final byte: siblings under a single leaf-parent /// (`S`) prefix. /// -/// Real leaves are content-addressed, so two distinct messages share a +/// Real leaves are version-addressed, so two distinct messages share a /// 31-byte prefix only under a hash-prefix collision; these hand-picked /// paths let a test construct that shape deliberately. fn leaf_sibling_path(last: u8) -> Path { @@ -526,7 +529,7 @@ fn root_with_ceiling(node: Option>, ceiling: Version) -> crate: /// leaving the store's own declared ceiling untouched — the shape only a /// nonconforming implementation can then transmit. The margin bounds the /// honest ticks a test may perform afterward without containing the -/// escape. Returns the root plus the escaped leaf's content-addressed path +/// escape. Returns the root plus the escaped leaf's version-derived path /// and its version. pub fn poisoned_root( party: &Party, @@ -541,7 +544,7 @@ pub fn poisoned_root( for _ in 0..ESCAPE_MARGIN { escaped.tick(party); } - let path = Path::for_leaf(&escaped, message.bytes()); + let path = Path::for_leaf(&escaped); let root = root_with_ceiling( act( None, diff --git a/src/tree/key.rs b/src/tree/key.rs deleted file mode 100644 index 4985c4c2a..000000000 --- a/src/tree/key.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::fmt::Debug; - -use borsh::{BorshDeserialize, BorshSerialize}; - -use super::typed; - -/// An opaque key uniquely identifying a message. -/// -/// A key is a unique pointer to the causal moment its message was -/// introduced by the party that sent it: it binds the send's version -/// (fresh on every send) to the message's content. Keys are therefore -/// unique across the entire history of the universe, byte-identical -/// re-sends are distinct messages under distinct keys, and the same -/// *send* has the same key on every replica. Keys come back out of the -/// observers and [`Snapshot`](crate::Snapshot) iteration; they go into -/// [`redact`](crate::Rumors::redact) and [`get`](crate::Snapshot::get). A -/// key is freely persistable as its raw 32 bytes -/// ([`as_bytes`](Self::as_bytes), [`From<[u8; 32]>`](Self#impl-From<[u8;+32]>-for-Key)). -#[derive(BorshSerialize, BorshDeserialize, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[repr(transparent)] -pub struct Key(pub(crate) [u8; 32]); - -/// Hex-encodes the 32-byte key as a lowercase string, with no surrounding -/// punctuation. Convenient in logs and assertion messages. -impl Debug for Key { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - hex::encode(self.0).fmt(f) - } -} - -/// The same lowercase hex as the [`Debug`] form. -impl std::fmt::Display for Key { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&hex::encode(self.0)) - } -} - -impl Key { - /// The raw 32 bytes: the leaf's content-addressed path. - pub fn as_bytes(&self) -> &[u8; 32] { - &self.0 - } -} - -/// Reconstitutes a key from its raw bytes (for example, one persisted for a -/// later redaction). -/// -/// A key that never named a live message is harmless: lookups miss and -/// redactions are no-ops. -impl From<[u8; 32]> for Key { - fn from(bytes: [u8; 32]) -> Self { - Self(bytes) - } -} - -impl From for [u8; 32] { - fn from(key: Key) -> Self { - key.0 - } -} - -impl From for Key { - fn from(path: typed::Path) -> Self { - Self(<[u8; 32]>::from(path)) - } -} - -impl From for typed::Path { - fn from(id: Key) -> Self { - typed::Path::from(id.0) - } -} diff --git a/src/tree/mirror/alternating/backend/remote.rs b/src/tree/mirror/alternating/backend/remote.rs index 40c70fb69..a54219cb2 100644 --- a/src/tree/mirror/alternating/backend/remote.rs +++ b/src/tree/mirror/alternating/backend/remote.rs @@ -23,7 +23,7 @@ //! //! # Framing //! -//! Each borsh-encoded message is shipped as a single length-delimited frame +//! Each encoded message is shipped as a single length-delimited frame //! (4-byte big-endian length prefix) through //! [`crate::tree::mirror::framing`]'s exact-read //! [`FrameRead`]/[`FrameWrite`]: the protocol's height schedule names the @@ -50,7 +50,7 @@ use std::marker::PhantomData; use tokio::io::{AsyncRead, AsyncWrite}; -use borsh::{BorshDeserialize, BorshSerialize}; +use crate::tree::wire; use crate::Error; use crate::tree::mirror::framing::{FrameRead, FrameWrite}; @@ -64,6 +64,8 @@ use super::super::{ protocol::{self, Step}, }; +use serde::Serialize; +use serde::de::DeserializeOwned; /// The version state for an [`Exchange`] which has just been initialized but /// has not yet connected. pub struct Start; @@ -123,7 +125,7 @@ impl protocol::Stage for Exchange protocol::Stage for Exchange(writer: &mut FrameWrite, msg: &M) -> Result<(), Error> where W: AsyncWrite + Unpin + Send, - M: BorshSerialize, + M: wire::Encode, { - let mut buf = Vec::new(); - msg.serialize(&mut buf).map_err(Error::Io)?; + let buf = wire::to_vec(msg).map_err(Error::Io)?; writer.frame(&buf).await.map_err(Error::Io)?; Ok(()) } -/// Pull one length-delimited frame off the wire and borsh-decode it as `M`. +/// Pull one length-delimited frame off the wire and decode it as `M`. /// /// A peer that closes the stream instead of sending the message — cleanly /// or mid-frame — surfaces as an -/// [`UnexpectedEof`](borsh::io::ErrorKind::UnexpectedEof) borsh I/O error. +/// [`UnexpectedEof`](std::io::ErrorKind::UnexpectedEof) I/O error. pub(super) async fn recv_msg(reader: &mut FrameRead) -> Result where R: AsyncRead + Unpin + Send, - M: BorshDeserialize, + M: wire::Decode, { let frame = reader .frame() .await .map_err(|e| match e.kind() { - borsh::io::ErrorKind::UnexpectedEof => borsh::io::Error::new( - borsh::io::ErrorKind::UnexpectedEof, + std::io::ErrorKind::UnexpectedEof => std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, "peer closed before sending expected message", ), _ => e, }) .map_err(Error::Io)?; - M::try_from_slice(&frame).map_err(Error::Io) + wire::from_slice(&frame).map_err(Error::Io) } // One protocol-trait impl block per trait, each at the specific height it @@ -171,7 +172,7 @@ impl protocol::Accept for Exchange where R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, - T: BorshSerialize + BorshDeserialize + Send + Sync, + T: Serialize + DeserializeOwned + Send + Sync, { type Next = Exchange; @@ -209,10 +210,10 @@ where impl protocol::Initiator for Exchange where - T: BorshDeserialize + Send + Sync, + T: DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, - Node: BorshDeserialize, + Node: wire::Decode, { type Next = Exchange; @@ -231,10 +232,10 @@ where impl protocol::Responder for Exchange where - T: BorshDeserialize + Send + Sync, + T: DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, - Node: BorshDeserialize, + Node: wire::Decode, { type Next = Exchange; @@ -259,10 +260,10 @@ where impl protocol::OpenInitiator for Exchange where - T: BorshDeserialize + Send + Sync, + T: DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, - Node: BorshDeserialize, + Node: wire::Decode, { type Next = Exchange; @@ -293,13 +294,13 @@ where impl protocol::Exchange for Exchange>> where - T: BorshDeserialize + Send + Sync, + T: DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, H: Height, S: Height, S>: Height, - Node>: BorshDeserialize, + Node>: wire::Decode, // Assumed at impl-validation time so we don't have to case-analyze `H` // here: at use sites `H` is concrete and one of the three blanket impls // in `super::protocol` discharges it. @@ -342,7 +343,7 @@ where impl protocol::CloseResponder for Exchange> where - T: BorshDeserialize + Send + Sync, + T: DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, { @@ -383,7 +384,7 @@ where impl protocol::CompleteInitiator for Exchange where - T: BorshDeserialize + Send + Sync, + T: DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, { diff --git a/src/tree/mirror/alternating/backend/remote/tests.rs b/src/tree/mirror/alternating/backend/remote/tests.rs index 0e061b217..1dfa344fb 100644 --- a/src/tree/mirror/alternating/backend/remote/tests.rs +++ b/src/tree/mirror/alternating/backend/remote/tests.rs @@ -1,7 +1,7 @@ //! Ingress validation of the V1 framed-message reader. //! //! Every V1 wire message reaches the protocol through [`recv_msg`]: one -//! length-delimited frame pulled off the transport, then one exact borsh +//! length-delimited frame pulled off the transport, then one exact //! decode of the frame's body. Both halves parse peer-controlled bytes, so //! this suite feeds the reader crafted frames — truncations at each //! structural boundary, length lies in both directions, trailing garbage — @@ -11,7 +11,6 @@ //! `alternating/message/tests.rs`; the exact wire bytes in //! `alternating/wire_snapshot.rs`. -use borsh::BorshDeserialize; use proptest::collection::vec; use proptest::prelude::*; @@ -20,6 +19,7 @@ use super::{FrameRead, recv_msg}; use crate::tree::arb::nth_party; use crate::tree::mirror::framing::LENGTH_HEADER_LEN; use crate::tree::typed::height::UnderRoot; +use crate::tree::wire; use crate::{Error, Version}; /// Length-delimit one frame body exactly as [`super::send_msg`] does. @@ -31,7 +31,7 @@ fn frame(body: &[u8]) -> Vec { } /// Pull one message from crafted wire bytes through the production ingress. -fn recv(bytes: &[u8]) -> Result { +fn recv(bytes: &[u8]) -> Result { pollster::block_on(async { let mut reader = FrameRead::new(bytes); recv_msg::(&mut reader).await @@ -44,11 +44,11 @@ fn handshake_bytes() -> Vec { let party = nth_party(0); let mut version = Version::new(); version.tick(&party); - borsh::to_vec(&message::Handshake { version }).expect("test handshakes encode") + wire::to_vec(&message::Handshake { version }).expect("test handshakes encode") } /// Unwrap the sole error variant this ingress can produce. -fn io_error(result: Result<(), Error>) -> borsh::io::Error { +fn io_error(result: Result<(), Error>) -> std::io::Error { match result { Err(Error::Io(error)) => error, other => panic!("the framed ingress fails as Error::Io, got {other:?}"), @@ -64,7 +64,7 @@ fn io_error(result: Result<(), Error>) -> borsh::io::Error { #[test] fn close_before_a_message_is_a_typed_eof() { let error = io_error(recv::(&[]).map(|_| ())); - assert_eq!(error.kind(), borsh::io::ErrorKind::UnexpectedEof); + assert_eq!(error.kind(), std::io::ErrorKind::UnexpectedEof); assert!( error.to_string().contains("peer closed"), "the boundary close carries its diagnosis, got {error}", @@ -81,7 +81,7 @@ fn truncated_length_header_is_a_typed_eof() { let error = io_error(recv::(&vec![0; cut]).map(|_| ())); assert_eq!( error.kind(), - borsh::io::ErrorKind::UnexpectedEof, + std::io::ErrorKind::UnexpectedEof, "cut after {cut} header bytes must be an unexpected EOF", ); } @@ -98,7 +98,7 @@ fn over_declared_frame_is_a_typed_eof() { bytes.extend_from_slice(&[1, 2, 3, 4]); let error = io_error(recv::(&bytes).map(|_| ())); - assert_eq!(error.kind(), borsh::io::ErrorKind::UnexpectedEof); + assert_eq!(error.kind(), std::io::ErrorKind::UnexpectedEof); } /// A message cut short inside an honestly sized frame is a typed error. @@ -113,7 +113,7 @@ fn under_declared_frame_is_a_typed_error() { body.truncate(body.len() - 1); let error = io_error(recv::(&frame(&body)).map(|_| ())); - assert_eq!(error.kind(), borsh::io::ErrorKind::UnexpectedEof); + assert_eq!(error.kind(), std::io::ErrorKind::UnexpectedEof); } /// A frame with bytes after its message is rejected as non-canonical. @@ -127,7 +127,7 @@ fn trailing_frame_bytes_are_rejected() { body.push(0xFF); let error = io_error(recv::(&frame(&body)).map(|_| ())); - assert_eq!(error.kind(), borsh::io::ErrorKind::InvalidData); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidData); } /// Reading one message consumes exactly its frame, leaving later bytes @@ -156,7 +156,7 @@ proptest! { /// for every V1 message type — never a panic. /// /// The frame is honestly sized around an arbitrary body, so the fuzz - /// lands on the borsh body decoders (the version bit codec, the channel + /// lands on the body decoders (the version bit codec, the channel /// order checks, the typed node reconstruction) rather than on the /// allocator via a lied length header — the header lies are pinned /// deterministically above. Decoding a slice always terminates, so a diff --git a/src/tree/mirror/alternating/message.rs b/src/tree/mirror/alternating/message.rs index 454f89b85..8b05e3fe0 100644 --- a/src/tree/mirror/alternating/message.rs +++ b/src/tree/mirror/alternating/message.rs @@ -1,8 +1,8 @@ //! # Wire format //! -//! Each message is borsh-encoded. The encoding is canonical (one byte sequence -//! per value) and reflects the in-memory representation directly. Container -//! lengths are `u32` little-endian. +//! Each message is encoded by the tree's [`wire`] +//! codec: explicit structural framing whose variable-width atoms are +//! single CBOR values. Container lengths are `u32` little-endian. //! //! ## Atoms //! @@ -12,20 +12,20 @@ //! no length prefix. //! - [`typed::Prefix`](crate::tree::typed::Prefix): exactly `32 − //! H::HEIGHT` raw bytes, no length prefix (the type pins the byte count). -//! - [`Version`] and [`Message`](crate::message::Message): -//! their existing borsh shapes (see those types). A `Message` serializes -//! byte-identically to its inner `T`. +//! - [`Version`] and [`Message`](crate::message::Message): one CBOR +//! value each — a byte string wrapping the version's canonical encoding, +//! and a byte string wrapping the message's cached CBOR payload — +//! self-delimiting by CBOR's own length headers. //! - `Vec<_>`: `u32` length followed by each element in order. Every channel //! is a length-prefixed `Vec`; on deserialize the decoder rejects any -//! frame whose entries are not strictly ascending in canonical order -//! (which also rejects duplicates), so each value has exactly one -//! encoding. +//! frame whose entries are not strictly ascending order (which also +//! rejects duplicates). //! //! ## Typed [`Node`](crate::tree::typed::Node) //! -//! Encoded in its in-memory layout. The typed `BorshSerialize` impl is a -//! thin delegate over the untyped node's `serialize_to`, which is the -//! canonical encoder: +//! Encoded in its in-memory layout. The typed node's wire impl is a thin +//! delegate over the untyped node's `serialize_to`, which is the canonical +//! encoder: //! //! ```text //! NodeWire ::= @@ -41,7 +41,7 @@ //! running `prefix_len`. On the decode side, when `prefix_len > 0` the //! decoder peels one head byte and recurses at the next-finer typed height, //! synthesizing the `prefix_len − 1` byte for the inner reader via -//! [`borsh::io::Read::chain`], so the wire carries one `prefix_len` byte +//! [`std::io::Read::chain`], so the wire carries one `prefix_len` byte //! per top-of-chain rather than one per typed level. //! //! Multi-child branches always carry at least two children; singletons @@ -67,12 +67,12 @@ //! //! ## Messages //! -//! Each of the five message types below is the borsh concatenation of its +//! Each of the five message types below is the concatenation of its //! fields in source order. There is no length framing between messages on //! the wire: the protocol's height schedule names the type each side expects //! next. -use borsh::{BorshDeserialize, BorshSerialize}; +use crate::tree::wire::{self, Decode, Encode}; use crate::Version; use crate::tree::typed::{ @@ -80,6 +80,7 @@ use crate::tree::typed::{ height::{Height, Root, S, UnderRoot, Z}, }; +use serde::de::DeserializeOwned; #[cfg(test)] mod tests; @@ -99,16 +100,15 @@ pub struct Handshake { pub version: Version, } -impl BorshSerialize for Handshake { - fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { - self.version.serialize(writer)?; - Ok(()) +impl Encode for Handshake { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + self.version.write_wire(writer) } } -impl BorshDeserialize for Handshake { - fn deserialize_reader(reader: &mut R) -> borsh::io::Result { - let version = Version::deserialize_reader(reader)?; +impl Decode for Handshake { + fn read_wire(reader: &mut R) -> std::io::Result { + let version = Version::read_wire(reader)?; Ok(Self { version }) } } @@ -121,14 +121,20 @@ impl BorshDeserialize for Handshake { /// empty). Distinct from `Opening` only by height, /// and from [`Exchange`] by the absence of `providing`/`requested`, which /// cannot be populated until at least one round has passed. -#[derive(Clone, Default, BorshSerialize)] +#[derive(Clone, Default)] pub struct Initiate { pub uncertain: Vec<(Prefix, Hash)>, } -impl BorshDeserialize for Initiate { - fn deserialize_reader(reader: &mut R) -> borsh::io::Result { - let uncertain = Vec::deserialize_reader(reader)?; +impl Encode for Initiate { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + self.uncertain.write_wire(writer) + } +} + +impl Decode for Initiate { + fn read_wire(reader: &mut R) -> std::io::Result { + let uncertain = Vec::read_wire(reader)?; verify_pairs_canonical(&uncertain, "Initiate.uncertain")?; Ok(Self { uncertain }) } @@ -146,14 +152,20 @@ impl BorshDeserialize for Initiate { /// separate entry point from the steady-state `exchange`, so the latter can /// assume every uncertain hash describes a parent the receiver has already /// acknowledged. -#[derive(Clone, Default, BorshSerialize)] +#[derive(Clone, Default)] pub struct Opening { pub uncertain: Vec<(Prefix, Hash)>, } -impl BorshDeserialize for Opening { - fn deserialize_reader(reader: &mut R) -> borsh::io::Result { - let uncertain = Vec::deserialize_reader(reader)?; +impl Encode for Opening { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + self.uncertain.write_wire(writer) + } +} + +impl Decode for Opening { + fn read_wire(reader: &mut R) -> std::io::Result { + let uncertain = Vec::read_wire(reader)?; verify_pairs_canonical(&uncertain, "Opening.uncertain")?; Ok(Self { uncertain }) } @@ -195,37 +207,36 @@ where pub uncertain: Vec<(Prefix, Hash)>, } -impl BorshSerialize for Exchange +impl Encode for Exchange where S: Height, H: Height, { - fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { - self.providing.serialize(writer)?; - self.requested.serialize(writer)?; - self.uncertain.serialize(writer)?; - Ok(()) + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + self.providing.write_wire(writer)?; + self.requested.write_wire(writer)?; + self.uncertain.write_wire(writer) } } -// `Node>: BorshDeserialize` reduces inductively to -// `Node: BorshDeserialize` and bottoms at `Z`, so with `H` left -// generic the proof obligation doesn't terminate during inference. We -// thread `Node>: BorshDeserialize` through as an explicit -// bound so the caller — who knows `H` concretely — discharges it. -impl BorshDeserialize for Exchange +// `Node>: Decode` reduces inductively to `Node: Decode` +// and bottoms at `Z`, so with `H` left generic the proof obligation +// doesn't terminate during inference. We thread `Node>: Decode` +// through as an explicit bound so the caller — who knows `H` concretely — +// discharges it. +impl Decode for Exchange where - T: BorshDeserialize, + T: DeserializeOwned, S: Height, H: Height, - Node>: BorshDeserialize, + Node>: Decode, { - fn deserialize_reader(reader: &mut R) -> borsh::io::Result { - let providing: Providing> = BorshDeserialize::deserialize_reader(reader)?; + fn read_wire(reader: &mut R) -> std::io::Result { + let providing: Providing> = Decode::read_wire(reader)?; verify_pairs_canonical(&providing, "Exchange.providing")?; - let requested: Vec>> = BorshDeserialize::deserialize_reader(reader)?; + let requested: Vec>> = Decode::read_wire(reader)?; verify_keys_canonical(&requested, "Exchange.requested")?; - let uncertain: Vec<(Prefix, Hash)> = BorshDeserialize::deserialize_reader(reader)?; + let uncertain: Vec<(Prefix, Hash)> = Decode::read_wire(reader)?; verify_pairs_canonical(&uncertain, "Exchange.uncertain")?; Ok(Self { providing, @@ -290,22 +301,21 @@ pub struct Closing { pub requested: Vec>, } -impl BorshSerialize for Closing { - fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { - self.providing.serialize(writer)?; - self.requested.serialize(writer)?; - Ok(()) +impl Encode for Closing { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + self.providing.write_wire(writer)?; + self.requested.write_wire(writer) } } -impl BorshDeserialize for Closing +impl Decode for Closing where - T: BorshDeserialize, + T: DeserializeOwned, { - fn deserialize_reader(reader: &mut R) -> borsh::io::Result { - let providing: Providing = BorshDeserialize::deserialize_reader(reader)?; + fn read_wire(reader: &mut R) -> std::io::Result { + let providing: Providing = Decode::read_wire(reader)?; verify_pairs_canonical(&providing, "Closing.providing")?; - let requested: Vec> = BorshDeserialize::deserialize_reader(reader)?; + let requested: Vec> = Decode::read_wire(reader)?; verify_keys_canonical(&requested, "Closing.requested")?; Ok(Self { providing, @@ -338,18 +348,18 @@ pub struct Complete { pub providing: Providing, } -impl BorshSerialize for Complete { - fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { - self.providing.serialize(writer) +impl Encode for Complete { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + self.providing.write_wire(writer) } } -impl BorshDeserialize for Complete +impl Decode for Complete where - T: BorshDeserialize, + T: DeserializeOwned, { - fn deserialize_reader(reader: &mut R) -> borsh::io::Result { - let providing: Providing = BorshDeserialize::deserialize_reader(reader)?; + fn read_wire(reader: &mut R) -> std::io::Result { + let providing: Providing = Decode::read_wire(reader)?; verify_pairs_canonical(&providing, "Complete.providing")?; Ok(Self { providing }) } @@ -366,11 +376,8 @@ impl Default for Complete { /// An out-of-order or duplicated wire channel: the canonical encoding admits /// exactly one byte sequence per value, so a peer that reorders or pads is /// rejected before its content is acted on. -fn not_canonical(what: &'static str) -> borsh::io::Error { - borsh::io::Error::new( - borsh::io::ErrorKind::InvalidData, - format!("{what} not in strictly ascending order"), - ) +fn not_canonical(what: &'static str) -> std::io::Error { + wire::invalid(format!("{what} not in strictly ascending order")) } /// Require key→value pairs to be in strictly ascending key order (rejecting @@ -378,7 +385,7 @@ fn not_canonical(what: &'static str) -> borsh::io::Error { pub(crate) fn verify_pairs_canonical( pairs: &[(K, V)], what: &'static str, -) -> borsh::io::Result<()> { +) -> std::io::Result<()> { if pairs.windows(2).any(|w| w[0].0 >= w[1].0) { return Err(not_canonical(what)); } @@ -387,10 +394,7 @@ pub(crate) fn verify_pairs_canonical( /// Require keys to be in strictly ascending order (rejecting duplicates): the /// `requested` channel. -pub(crate) fn verify_keys_canonical( - keys: &[K], - what: &'static str, -) -> borsh::io::Result<()> { +pub(crate) fn verify_keys_canonical(keys: &[K], what: &'static str) -> std::io::Result<()> { if keys.windows(2).any(|w| w[0] >= w[1]) { return Err(not_canonical(what)); } diff --git a/src/tree/mirror/alternating/message/tests.rs b/src/tree/mirror/alternating/message/tests.rs index 429eebe2c..fe214dba3 100644 --- a/src/tree/mirror/alternating/message/tests.rs +++ b/src/tree/mirror/alternating/message/tests.rs @@ -1,4 +1,4 @@ -//! Borsh round-trip property tests for the five mirror message types, plus the +//! Wire round-trip property tests for the five mirror message types, plus the //! canonical-order rejection each channel enforces on deserialize. //! //! Every channel is a length-prefixed `Vec` that must arrive in strictly @@ -11,7 +11,6 @@ use std::collections::{BTreeMap, BTreeSet}; -use borsh::BorshDeserialize; use proptest::collection::vec; use proptest::prelude::*; @@ -20,13 +19,14 @@ use crate::message::Message; use crate::tree::arb::{arb_root_node, arb_version, nth_party}; use crate::tree::typed::height::{Height, Root, S, Z}; use crate::tree::typed::{Hash, Node, Prefix, hash::MERKLE_HASH_LEN}; +use crate::tree::wire; use super as message; /// Build a `Prefix` from a raw byte slice (length `32 - H::HEIGHT`). fn prefix_from_bytes(bytes: &[u8]) -> Prefix { assert_eq!(bytes.len(), 32 - H::HEIGHT); - Prefix::::try_from_slice(bytes).expect("known-valid prefix bytes") + wire::from_slice(bytes).expect("known-valid prefix bytes") } fn arb_prefix() -> BoxedStrategy> { @@ -78,25 +78,25 @@ fn canonical_keys(keys: Vec>) -> Vec> { proptest! { /// `Initiate.uncertain` round-trips, fed in canonical ascending order. #[test] - fn initiate_borsh_round_trip( + fn initiate_wire_round_trip( entries in vec((arb_prefix::(), arb_hash()), 0..=4), ) { let uncertain = canonical_pairs(entries); let m = message::Initiate { uncertain: uncertain.clone() }; - let bytes = borsh::to_vec(&m).unwrap(); - let decoded = message::Initiate::try_from_slice(&bytes).unwrap(); + let bytes = wire::to_vec(&m).unwrap(); + let decoded = wire::from_slice::(&bytes).unwrap(); prop_assert_eq!(decoded.uncertain, uncertain); } /// `Opening.uncertain` round-trips, fed in canonical ascending order. #[test] - fn opening_borsh_round_trip( + fn opening_wire_round_trip( entries in vec((arb_prefix::(), arb_hash()), 0..=4), ) { let uncertain = canonical_pairs(entries); let m = message::Opening { uncertain: uncertain.clone() }; - let bytes = borsh::to_vec(&m).unwrap(); - let decoded = message::Opening::try_from_slice(&bytes).unwrap(); + let bytes = wire::to_vec(&m).unwrap(); + let decoded = wire::from_slice::(&bytes).unwrap(); prop_assert_eq!(decoded.uncertain, uncertain); } @@ -104,7 +104,7 @@ proptest! { /// height (populated from `arb_root_node`), an ascending `requested` at /// `Root`, and ascending `uncertain` hashes at `UnderRoot`. #[test] - fn exchange_borsh_round_trip( + fn exchange_wire_round_trip( providing_entries in vec( (arb_prefix::(), arb_root_node(0, 1..=4).prop_filter("non-empty", |n| n.is_some())), 0..=2, @@ -125,9 +125,9 @@ proptest! { requested: requested.clone(), uncertain: uncertain.clone(), }; - let bytes = borsh::to_vec(&m).unwrap(); + let bytes = wire::to_vec(&m).unwrap(); let decoded = - message::Exchange::<(), message::UnderRoot>::try_from_slice(&bytes).unwrap(); + wire::from_slice::>(&bytes).unwrap(); prop_assert_eq!(decoded.providing, providing); prop_assert_eq!(decoded.requested, requested); prop_assert_eq!(decoded.uncertain, uncertain); @@ -136,7 +136,7 @@ proptest! { /// `Closing` carries leaf-height `providing` and an ascending /// `requested`, both at `Z`. #[test] - fn closing_borsh_round_trip( + fn closing_wire_round_trip( providing_entries in vec((arb_prefix::(), arb_leaf()), 0..=4), requested in vec(arb_prefix::(), 0..=4), ) { @@ -146,8 +146,8 @@ proptest! { providing: providing.clone(), requested: requested.clone(), }; - let bytes = borsh::to_vec(&m).unwrap(); - let decoded = message::Closing::<()>::try_from_slice(&bytes).unwrap(); + let bytes = wire::to_vec(&m).unwrap(); + let decoded = wire::from_slice::>(&bytes).unwrap(); prop_assert_eq!(decoded.providing, providing); prop_assert_eq!(decoded.requested, requested); } @@ -155,13 +155,13 @@ proptest! { /// `Complete` carries only `providing`, at leaf (`Z`) height where a `Node` /// is exactly a leaf. #[test] - fn complete_borsh_round_trip( + fn complete_wire_round_trip( providing_entries in vec((arb_prefix::(), arb_leaf()), 0..=4), ) { let providing = canonical_providing(providing_entries); let m: message::Complete<()> = message::Complete { providing: providing.clone() }; - let bytes = borsh::to_vec(&m).unwrap(); - let decoded = message::Complete::<()>::try_from_slice(&bytes).unwrap(); + let bytes = wire::to_vec(&m).unwrap(); + let decoded = wire::from_slice::>(&bytes).unwrap(); prop_assert_eq!(decoded.providing, providing); } @@ -181,8 +181,8 @@ proptest! { permuted.rotate_left(rotate % canonical.len()); prop_assume!(permuted != canonical); let m = message::Complete::<()> { providing: permuted }; - let bytes = borsh::to_vec(&m).unwrap(); - prop_assert!(message::Complete::<()>::try_from_slice(&bytes).is_err()); + let bytes = wire::to_vec(&m).unwrap(); + prop_assert!(wire::from_slice::>(&bytes).is_err()); } } @@ -203,8 +203,8 @@ fn providing_rejects_duplicate_prefix() { let m = message::Complete::<()> { providing: vec![(prefix, leaf.clone()), (prefix, leaf)], }; - let bytes = borsh::to_vec(&m).unwrap(); - assert!(message::Complete::<()>::try_from_slice(&bytes).is_err()); + let bytes = wire::to_vec(&m).unwrap(); + assert!(wire::from_slice::>(&bytes).is_err()); } /// A `requested` frame whose prefixes descend is rejected. @@ -217,8 +217,8 @@ fn requested_rejects_descending_order() { prefix_from_bytes::(&[1u8; 32]), ], }; - let bytes = borsh::to_vec(&m).unwrap(); - assert!(message::Closing::<()>::try_from_slice(&bytes).is_err()); + let bytes = wire::to_vec(&m).unwrap(); + assert!(wire::from_slice::>(&bytes).is_err()); } /// An `uncertain` frame with a duplicate prefix is rejected. @@ -230,8 +230,8 @@ fn uncertain_rejects_duplicate_prefix() { (prefix_from_bytes::(&[]), Hash([1; MERKLE_HASH_LEN])), ], }; - let bytes = borsh::to_vec(&m).unwrap(); - assert!(message::Initiate::try_from_slice(&bytes).is_err()); + let bytes = wire::to_vec(&m).unwrap(); + assert!(wire::from_slice::(&bytes).is_err()); } // The `providing` channels carry whole wire-encoded nodes, so the node @@ -248,8 +248,8 @@ fn uncertain_rejects_duplicate_prefix() { /// past the leaf floor. #[test] fn node_prefix_exceeding_height_is_rejected() { - let error = Node::<(), S>::try_from_slice(&[2]).unwrap_err(); - assert_eq!(error.kind(), borsh::io::ErrorKind::InvalidData); + let error = wire::from_slice::>>(&[2]).unwrap_err(); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidData); } /// A branch declaring 257 children is rejected before any child is read. @@ -260,8 +260,8 @@ fn node_prefix_exceeding_height_is_rejected() { /// cannot all be placed. #[test] fn node_child_count_overflow_is_rejected() { - let error = Node::<(), S>::try_from_slice(&[0, 255]).unwrap_err(); - assert_eq!(error.kind(), borsh::io::ErrorKind::InvalidData); + let error = wire::from_slice::>>(&[0, 255]).unwrap_err(); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidData); } /// Branch radices that fail to strictly ascend are rejected at the radix. @@ -271,7 +271,7 @@ fn node_child_count_overflow_is_rejected() { /// (`InvalidData`) rather than let one branch have two encodings. #[test] fn node_descending_radices_are_rejected() { - let leaf = borsh::to_vec(&Node::<(), Z>::leaf(one_version(), Message::new(()))).unwrap(); + let leaf = wire::to_vec(&Node::<(), Z>::leaf(one_version(), Message::new(()))).unwrap(); // prefix_len 0, count_minus_two 0 (two children), radix 5, its leaf, // then a second radix that does not ascend. let mut bytes = vec![0, 0, 5]; @@ -279,6 +279,6 @@ fn node_descending_radices_are_rejected() { bytes.push(5); bytes.extend_from_slice(&leaf); - let error = Node::<(), S>::try_from_slice(&bytes).unwrap_err(); - assert_eq!(error.kind(), borsh::io::ErrorKind::InvalidData); + let error = wire::from_slice::>>(&bytes).unwrap_err(); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidData); } diff --git a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__message_closing_populated.snap b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__message_closing_populated.snap index 0ad6f4948..fbb971a40 100644 --- a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__message_closing_populated.snap +++ b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__message_closing_populated.snap @@ -4,6 +4,6 @@ expression: snap(&m) --- 0000: 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -0020: 00 00 00 00 00 77 01 00 00 00 ff ff ff ff ff ff +0020: 00 00 00 00 00 41 77 41 f6 01 00 00 00 ff ff ff 0030: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff -0040: ff ff ff ff ff ff ff ff ff ff +0040: ff ff ff ff ff ff ff ff ff ff ff ff ff diff --git a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__message_complete_populated.snap b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__message_complete_populated.snap index 4e47041f2..8def01a84 100644 --- a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__message_complete_populated.snap +++ b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__message_complete_populated.snap @@ -4,4 +4,4 @@ expression: snap(&m) --- 0000: 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -0020: 00 00 00 00 00 77 +0020: 00 00 00 00 00 41 77 41 f6 diff --git a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__message_exchange_populated.snap b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__message_exchange_populated.snap index 323c64039..acdf761f3 100644 --- a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__message_exchange_populated.snap +++ b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__message_exchange_populated.snap @@ -4,6 +4,7 @@ expression: snap(&m) --- 0000: 01 00 00 00 1e 1d 1c 1b 1a 19 18 17 16 15 14 13 0010: 12 11 10 0f 0e 0d 0c 0b 0a 09 08 07 06 05 04 03 -0020: 02 01 00 00 01 01 ab 77 02 01 ab 77 01 00 00 00 -0030: 01 00 00 00 cc 03 03 03 03 03 03 03 03 03 03 03 -0040: 03 03 03 03 03 03 03 03 03 03 03 03 03 +0020: 02 01 00 00 01 01 ab 41 77 41 f6 02 01 ab 41 77 +0030: 41 f6 01 00 00 00 01 00 00 00 cc 03 03 03 03 03 +0040: 03 03 03 03 03 03 03 03 03 03 03 03 03 03 03 03 +0050: 03 03 03 diff --git a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_root_single_leaf_full_compression.snap b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_root_single_leaf_full_compression.snap index 18c675592..bb853cb4a 100644 --- a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_root_single_leaf_full_compression.snap +++ b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_root_single_leaf_full_compression.snap @@ -4,4 +4,4 @@ expression: snap(&n) --- 0000: 20 1f 1e 1d 1c 1b 1a 19 18 17 16 15 14 13 12 11 0010: 10 0f 0e 0d 0c 0b 0a 09 08 07 06 05 04 03 02 01 -0020: 00 77 +0020: 00 41 77 41 f6 diff --git a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_root_two_leaves_branched_at_root.snap b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_root_two_leaves_branched_at_root.snap index cd6bf2718..d110a316f 100644 --- a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_root_two_leaves_branched_at_root.snap +++ b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_root_two_leaves_branched_at_root.snap @@ -4,6 +4,6 @@ expression: snap(&n) --- 0000: 00 00 01 1f 1e 1d 1c 1b 1a 19 18 17 16 15 14 13 0010: 12 11 10 0f 0e 0d 0c 0b 0a 09 08 07 06 05 04 03 -0020: 02 01 00 77 02 1f 1e 1d 1c 1b 1a 19 18 17 16 15 -0030: 14 13 12 11 10 0f 0e 0d 0c 0b 0a 09 08 07 06 05 -0040: 04 03 02 01 00 72 c0 +0020: 02 01 00 41 77 41 f6 02 1f 1e 1d 1c 1b 1a 19 18 +0030: 17 16 15 14 13 12 11 10 0f 0e 0d 0c 0b 0a 09 08 +0040: 07 06 05 04 03 02 01 00 42 72 c0 41 f6 diff --git a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_s_z_full_256_child_branch.snap b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_s_z_full_256_child_branch.snap index ff73a371d..8471b8a7f 100644 --- a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_s_z_full_256_child_branch.snap +++ b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_s_z_full_256_child_branch.snap @@ -2,82 +2,130 @@ source: src/tree/mirror/alternating/wire_snapshot.rs expression: snap(&n) --- -0000: 00 fe 00 00 77 01 00 72 c0 02 00 73 c0 03 00 71 -0010: 30 04 00 71 70 05 00 71 b0 06 00 71 f0 07 00 70 -0020: 8c 08 00 70 9c 09 00 70 ac 0a 00 70 bc 0b 00 70 -0030: cc 0c 00 70 dc 0d 00 70 ec 0e 00 70 fc 0f 00 70 -0040: 43 10 00 70 47 11 00 70 4b 12 00 70 4f 13 00 70 -0050: 53 14 00 70 57 15 00 70 5b 16 00 70 5f 17 00 70 -0060: 63 18 00 70 67 19 00 70 6b 1a 00 70 6f 1b 00 70 -0070: 73 1c 00 70 77 1d 00 70 7b 1e 00 70 7f 1f 00 70 -0080: 20 c0 20 00 70 21 c0 21 00 70 22 c0 22 00 70 23 -0090: c0 23 00 70 24 c0 24 00 70 25 c0 25 00 70 26 c0 -00a0: 26 00 70 27 c0 27 00 70 28 c0 28 00 70 29 c0 29 -00b0: 00 70 2a c0 2a 00 70 2b c0 2b 00 70 2c c0 2c 00 -00c0: 70 2d c0 2d 00 70 2e c0 2e 00 70 2f c0 2f 00 70 -00d0: 30 c0 30 00 70 31 c0 31 00 70 32 c0 32 00 70 33 -00e0: c0 33 00 70 34 c0 34 00 70 35 c0 35 00 70 36 c0 -00f0: 36 00 70 37 c0 37 00 70 38 c0 38 00 70 39 c0 39 -0100: 00 70 3a c0 3a 00 70 3b c0 3b 00 70 3c c0 3c 00 -0110: 70 3d c0 3d 00 70 3e c0 3e 00 70 3f c0 3f 00 70 -0120: 10 30 40 00 70 10 70 41 00 70 10 b0 42 00 70 10 -0130: f0 43 00 70 11 30 44 00 70 11 70 45 00 70 11 b0 -0140: 46 00 70 11 f0 47 00 70 12 30 48 00 70 12 70 49 -0150: 00 70 12 b0 4a 00 70 12 f0 4b 00 70 13 30 4c 00 -0160: 70 13 70 4d 00 70 13 b0 4e 00 70 13 f0 4f 00 70 -0170: 14 30 50 00 70 14 70 51 00 70 14 b0 52 00 70 14 -0180: f0 53 00 70 15 30 54 00 70 15 70 55 00 70 15 b0 -0190: 56 00 70 15 f0 57 00 70 16 30 58 00 70 16 70 59 -01a0: 00 70 16 b0 5a 00 70 16 f0 5b 00 70 17 30 5c 00 -01b0: 70 17 70 5d 00 70 17 b0 5e 00 70 17 f0 5f 00 70 -01c0: 18 30 60 00 70 18 70 61 00 70 18 b0 62 00 70 18 -01d0: f0 63 00 70 19 30 64 00 70 19 70 65 00 70 19 b0 -01e0: 66 00 70 19 f0 67 00 70 1a 30 68 00 70 1a 70 69 -01f0: 00 70 1a b0 6a 00 70 1a f0 6b 00 70 1b 30 6c 00 -0200: 70 1b 70 6d 00 70 1b b0 6e 00 70 1b f0 6f 00 70 -0210: 1c 30 70 00 70 1c 70 71 00 70 1c b0 72 00 70 1c -0220: f0 73 00 70 1d 30 74 00 70 1d 70 75 00 70 1d b0 -0230: 76 00 70 1d f0 77 00 70 1e 30 78 00 70 1e 70 79 -0240: 00 70 1e b0 7a 00 70 1e f0 7b 00 70 1f 30 7c 00 -0250: 70 1f 70 7d 00 70 1f b0 7e 00 70 1f f0 7f 00 70 -0260: 08 0c 80 00 70 08 1c 81 00 70 08 2c 82 00 70 08 -0270: 3c 83 00 70 08 4c 84 00 70 08 5c 85 00 70 08 6c -0280: 86 00 70 08 7c 87 00 70 08 8c 88 00 70 08 9c 89 -0290: 00 70 08 ac 8a 00 70 08 bc 8b 00 70 08 cc 8c 00 -02a0: 70 08 dc 8d 00 70 08 ec 8e 00 70 08 fc 8f 00 70 -02b0: 09 0c 90 00 70 09 1c 91 00 70 09 2c 92 00 70 09 -02c0: 3c 93 00 70 09 4c 94 00 70 09 5c 95 00 70 09 6c -02d0: 96 00 70 09 7c 97 00 70 09 8c 98 00 70 09 9c 99 -02e0: 00 70 09 ac 9a 00 70 09 bc 9b 00 70 09 cc 9c 00 -02f0: 70 09 dc 9d 00 70 09 ec 9e 00 70 09 fc 9f 00 70 -0300: 0a 0c a0 00 70 0a 1c a1 00 70 0a 2c a2 00 70 0a -0310: 3c a3 00 70 0a 4c a4 00 70 0a 5c a5 00 70 0a 6c -0320: a6 00 70 0a 7c a7 00 70 0a 8c a8 00 70 0a 9c a9 -0330: 00 70 0a ac aa 00 70 0a bc ab 00 70 0a cc ac 00 -0340: 70 0a dc ad 00 70 0a ec ae 00 70 0a fc af 00 70 -0350: 0b 0c b0 00 70 0b 1c b1 00 70 0b 2c b2 00 70 0b -0360: 3c b3 00 70 0b 4c b4 00 70 0b 5c b5 00 70 0b 6c -0370: b6 00 70 0b 7c b7 00 70 0b 8c b8 00 70 0b 9c b9 -0380: 00 70 0b ac ba 00 70 0b bc bb 00 70 0b cc bc 00 -0390: 70 0b dc bd 00 70 0b ec be 00 70 0b fc bf 00 70 -03a0: 0c 0c c0 00 70 0c 1c c1 00 70 0c 2c c2 00 70 0c -03b0: 3c c3 00 70 0c 4c c4 00 70 0c 5c c5 00 70 0c 6c -03c0: c6 00 70 0c 7c c7 00 70 0c 8c c8 00 70 0c 9c c9 -03d0: 00 70 0c ac ca 00 70 0c bc cb 00 70 0c cc cc 00 -03e0: 70 0c dc cd 00 70 0c ec ce 00 70 0c fc cf 00 70 -03f0: 0d 0c d0 00 70 0d 1c d1 00 70 0d 2c d2 00 70 0d -0400: 3c d3 00 70 0d 4c d4 00 70 0d 5c d5 00 70 0d 6c -0410: d6 00 70 0d 7c d7 00 70 0d 8c d8 00 70 0d 9c d9 -0420: 00 70 0d ac da 00 70 0d bc db 00 70 0d cc dc 00 -0430: 70 0d dc dd 00 70 0d ec de 00 70 0d fc df 00 70 -0440: 0e 0c e0 00 70 0e 1c e1 00 70 0e 2c e2 00 70 0e -0450: 3c e3 00 70 0e 4c e4 00 70 0e 5c e5 00 70 0e 6c -0460: e6 00 70 0e 7c e7 00 70 0e 8c e8 00 70 0e 9c e9 -0470: 00 70 0e ac ea 00 70 0e bc eb 00 70 0e cc ec 00 -0480: 70 0e dc ed 00 70 0e ec ee 00 70 0e fc ef 00 70 -0490: 0f 0c f0 00 70 0f 1c f1 00 70 0f 2c f2 00 70 0f -04a0: 3c f3 00 70 0f 4c f4 00 70 0f 5c f5 00 70 0f 6c -04b0: f6 00 70 0f 7c f7 00 70 0f 8c f8 00 70 0f 9c f9 -04c0: 00 70 0f ac fa 00 70 0f bc fb 00 70 0f cc fc 00 -04d0: 70 0f dc fd 00 70 0f ec fe 00 70 0f fc ff 00 70 -04e0: 04 03 +0000: 00 fe 00 00 41 77 41 f6 01 00 42 72 c0 41 f6 02 +0010: 00 42 73 c0 41 f6 03 00 42 71 30 41 f6 04 00 42 +0020: 71 70 41 f6 05 00 42 71 b0 41 f6 06 00 42 71 f0 +0030: 41 f6 07 00 42 70 8c 41 f6 08 00 42 70 9c 41 f6 +0040: 09 00 42 70 ac 41 f6 0a 00 42 70 bc 41 f6 0b 00 +0050: 42 70 cc 41 f6 0c 00 42 70 dc 41 f6 0d 00 42 70 +0060: ec 41 f6 0e 00 42 70 fc 41 f6 0f 00 42 70 43 41 +0070: f6 10 00 42 70 47 41 f6 11 00 42 70 4b 41 f6 12 +0080: 00 42 70 4f 41 f6 13 00 42 70 53 41 f6 14 00 42 +0090: 70 57 41 f6 15 00 42 70 5b 41 f6 16 00 42 70 5f +00a0: 41 f6 17 00 42 70 63 41 f6 18 00 42 70 67 41 f6 +00b0: 19 00 42 70 6b 41 f6 1a 00 42 70 6f 41 f6 1b 00 +00c0: 42 70 73 41 f6 1c 00 42 70 77 41 f6 1d 00 42 70 +00d0: 7b 41 f6 1e 00 42 70 7f 41 f6 1f 00 43 70 20 c0 +00e0: 41 f6 20 00 43 70 21 c0 41 f6 21 00 43 70 22 c0 +00f0: 41 f6 22 00 43 70 23 c0 41 f6 23 00 43 70 24 c0 +0100: 41 f6 24 00 43 70 25 c0 41 f6 25 00 43 70 26 c0 +0110: 41 f6 26 00 43 70 27 c0 41 f6 27 00 43 70 28 c0 +0120: 41 f6 28 00 43 70 29 c0 41 f6 29 00 43 70 2a c0 +0130: 41 f6 2a 00 43 70 2b c0 41 f6 2b 00 43 70 2c c0 +0140: 41 f6 2c 00 43 70 2d c0 41 f6 2d 00 43 70 2e c0 +0150: 41 f6 2e 00 43 70 2f c0 41 f6 2f 00 43 70 30 c0 +0160: 41 f6 30 00 43 70 31 c0 41 f6 31 00 43 70 32 c0 +0170: 41 f6 32 00 43 70 33 c0 41 f6 33 00 43 70 34 c0 +0180: 41 f6 34 00 43 70 35 c0 41 f6 35 00 43 70 36 c0 +0190: 41 f6 36 00 43 70 37 c0 41 f6 37 00 43 70 38 c0 +01a0: 41 f6 38 00 43 70 39 c0 41 f6 39 00 43 70 3a c0 +01b0: 41 f6 3a 00 43 70 3b c0 41 f6 3b 00 43 70 3c c0 +01c0: 41 f6 3c 00 43 70 3d c0 41 f6 3d 00 43 70 3e c0 +01d0: 41 f6 3e 00 43 70 3f c0 41 f6 3f 00 43 70 10 30 +01e0: 41 f6 40 00 43 70 10 70 41 f6 41 00 43 70 10 b0 +01f0: 41 f6 42 00 43 70 10 f0 41 f6 43 00 43 70 11 30 +0200: 41 f6 44 00 43 70 11 70 41 f6 45 00 43 70 11 b0 +0210: 41 f6 46 00 43 70 11 f0 41 f6 47 00 43 70 12 30 +0220: 41 f6 48 00 43 70 12 70 41 f6 49 00 43 70 12 b0 +0230: 41 f6 4a 00 43 70 12 f0 41 f6 4b 00 43 70 13 30 +0240: 41 f6 4c 00 43 70 13 70 41 f6 4d 00 43 70 13 b0 +0250: 41 f6 4e 00 43 70 13 f0 41 f6 4f 00 43 70 14 30 +0260: 41 f6 50 00 43 70 14 70 41 f6 51 00 43 70 14 b0 +0270: 41 f6 52 00 43 70 14 f0 41 f6 53 00 43 70 15 30 +0280: 41 f6 54 00 43 70 15 70 41 f6 55 00 43 70 15 b0 +0290: 41 f6 56 00 43 70 15 f0 41 f6 57 00 43 70 16 30 +02a0: 41 f6 58 00 43 70 16 70 41 f6 59 00 43 70 16 b0 +02b0: 41 f6 5a 00 43 70 16 f0 41 f6 5b 00 43 70 17 30 +02c0: 41 f6 5c 00 43 70 17 70 41 f6 5d 00 43 70 17 b0 +02d0: 41 f6 5e 00 43 70 17 f0 41 f6 5f 00 43 70 18 30 +02e0: 41 f6 60 00 43 70 18 70 41 f6 61 00 43 70 18 b0 +02f0: 41 f6 62 00 43 70 18 f0 41 f6 63 00 43 70 19 30 +0300: 41 f6 64 00 43 70 19 70 41 f6 65 00 43 70 19 b0 +0310: 41 f6 66 00 43 70 19 f0 41 f6 67 00 43 70 1a 30 +0320: 41 f6 68 00 43 70 1a 70 41 f6 69 00 43 70 1a b0 +0330: 41 f6 6a 00 43 70 1a f0 41 f6 6b 00 43 70 1b 30 +0340: 41 f6 6c 00 43 70 1b 70 41 f6 6d 00 43 70 1b b0 +0350: 41 f6 6e 00 43 70 1b f0 41 f6 6f 00 43 70 1c 30 +0360: 41 f6 70 00 43 70 1c 70 41 f6 71 00 43 70 1c b0 +0370: 41 f6 72 00 43 70 1c f0 41 f6 73 00 43 70 1d 30 +0380: 41 f6 74 00 43 70 1d 70 41 f6 75 00 43 70 1d b0 +0390: 41 f6 76 00 43 70 1d f0 41 f6 77 00 43 70 1e 30 +03a0: 41 f6 78 00 43 70 1e 70 41 f6 79 00 43 70 1e b0 +03b0: 41 f6 7a 00 43 70 1e f0 41 f6 7b 00 43 70 1f 30 +03c0: 41 f6 7c 00 43 70 1f 70 41 f6 7d 00 43 70 1f b0 +03d0: 41 f6 7e 00 43 70 1f f0 41 f6 7f 00 43 70 08 0c +03e0: 41 f6 80 00 43 70 08 1c 41 f6 81 00 43 70 08 2c +03f0: 41 f6 82 00 43 70 08 3c 41 f6 83 00 43 70 08 4c +0400: 41 f6 84 00 43 70 08 5c 41 f6 85 00 43 70 08 6c +0410: 41 f6 86 00 43 70 08 7c 41 f6 87 00 43 70 08 8c +0420: 41 f6 88 00 43 70 08 9c 41 f6 89 00 43 70 08 ac +0430: 41 f6 8a 00 43 70 08 bc 41 f6 8b 00 43 70 08 cc +0440: 41 f6 8c 00 43 70 08 dc 41 f6 8d 00 43 70 08 ec +0450: 41 f6 8e 00 43 70 08 fc 41 f6 8f 00 43 70 09 0c +0460: 41 f6 90 00 43 70 09 1c 41 f6 91 00 43 70 09 2c +0470: 41 f6 92 00 43 70 09 3c 41 f6 93 00 43 70 09 4c +0480: 41 f6 94 00 43 70 09 5c 41 f6 95 00 43 70 09 6c +0490: 41 f6 96 00 43 70 09 7c 41 f6 97 00 43 70 09 8c +04a0: 41 f6 98 00 43 70 09 9c 41 f6 99 00 43 70 09 ac +04b0: 41 f6 9a 00 43 70 09 bc 41 f6 9b 00 43 70 09 cc +04c0: 41 f6 9c 00 43 70 09 dc 41 f6 9d 00 43 70 09 ec +04d0: 41 f6 9e 00 43 70 09 fc 41 f6 9f 00 43 70 0a 0c +04e0: 41 f6 a0 00 43 70 0a 1c 41 f6 a1 00 43 70 0a 2c +04f0: 41 f6 a2 00 43 70 0a 3c 41 f6 a3 00 43 70 0a 4c +0500: 41 f6 a4 00 43 70 0a 5c 41 f6 a5 00 43 70 0a 6c +0510: 41 f6 a6 00 43 70 0a 7c 41 f6 a7 00 43 70 0a 8c +0520: 41 f6 a8 00 43 70 0a 9c 41 f6 a9 00 43 70 0a ac +0530: 41 f6 aa 00 43 70 0a bc 41 f6 ab 00 43 70 0a cc +0540: 41 f6 ac 00 43 70 0a dc 41 f6 ad 00 43 70 0a ec +0550: 41 f6 ae 00 43 70 0a fc 41 f6 af 00 43 70 0b 0c +0560: 41 f6 b0 00 43 70 0b 1c 41 f6 b1 00 43 70 0b 2c +0570: 41 f6 b2 00 43 70 0b 3c 41 f6 b3 00 43 70 0b 4c +0580: 41 f6 b4 00 43 70 0b 5c 41 f6 b5 00 43 70 0b 6c +0590: 41 f6 b6 00 43 70 0b 7c 41 f6 b7 00 43 70 0b 8c +05a0: 41 f6 b8 00 43 70 0b 9c 41 f6 b9 00 43 70 0b ac +05b0: 41 f6 ba 00 43 70 0b bc 41 f6 bb 00 43 70 0b cc +05c0: 41 f6 bc 00 43 70 0b dc 41 f6 bd 00 43 70 0b ec +05d0: 41 f6 be 00 43 70 0b fc 41 f6 bf 00 43 70 0c 0c +05e0: 41 f6 c0 00 43 70 0c 1c 41 f6 c1 00 43 70 0c 2c +05f0: 41 f6 c2 00 43 70 0c 3c 41 f6 c3 00 43 70 0c 4c +0600: 41 f6 c4 00 43 70 0c 5c 41 f6 c5 00 43 70 0c 6c +0610: 41 f6 c6 00 43 70 0c 7c 41 f6 c7 00 43 70 0c 8c +0620: 41 f6 c8 00 43 70 0c 9c 41 f6 c9 00 43 70 0c ac +0630: 41 f6 ca 00 43 70 0c bc 41 f6 cb 00 43 70 0c cc +0640: 41 f6 cc 00 43 70 0c dc 41 f6 cd 00 43 70 0c ec +0650: 41 f6 ce 00 43 70 0c fc 41 f6 cf 00 43 70 0d 0c +0660: 41 f6 d0 00 43 70 0d 1c 41 f6 d1 00 43 70 0d 2c +0670: 41 f6 d2 00 43 70 0d 3c 41 f6 d3 00 43 70 0d 4c +0680: 41 f6 d4 00 43 70 0d 5c 41 f6 d5 00 43 70 0d 6c +0690: 41 f6 d6 00 43 70 0d 7c 41 f6 d7 00 43 70 0d 8c +06a0: 41 f6 d8 00 43 70 0d 9c 41 f6 d9 00 43 70 0d ac +06b0: 41 f6 da 00 43 70 0d bc 41 f6 db 00 43 70 0d cc +06c0: 41 f6 dc 00 43 70 0d dc 41 f6 dd 00 43 70 0d ec +06d0: 41 f6 de 00 43 70 0d fc 41 f6 df 00 43 70 0e 0c +06e0: 41 f6 e0 00 43 70 0e 1c 41 f6 e1 00 43 70 0e 2c +06f0: 41 f6 e2 00 43 70 0e 3c 41 f6 e3 00 43 70 0e 4c +0700: 41 f6 e4 00 43 70 0e 5c 41 f6 e5 00 43 70 0e 6c +0710: 41 f6 e6 00 43 70 0e 7c 41 f6 e7 00 43 70 0e 8c +0720: 41 f6 e8 00 43 70 0e 9c 41 f6 e9 00 43 70 0e ac +0730: 41 f6 ea 00 43 70 0e bc 41 f6 eb 00 43 70 0e cc +0740: 41 f6 ec 00 43 70 0e dc 41 f6 ed 00 43 70 0e ec +0750: 41 f6 ee 00 43 70 0e fc 41 f6 ef 00 43 70 0f 0c +0760: 41 f6 f0 00 43 70 0f 1c 41 f6 f1 00 43 70 0f 2c +0770: 41 f6 f2 00 43 70 0f 3c 41 f6 f3 00 43 70 0f 4c +0780: 41 f6 f4 00 43 70 0f 5c 41 f6 f5 00 43 70 0f 6c +0790: 41 f6 f6 00 43 70 0f 7c 41 f6 f7 00 43 70 0f 8c +07a0: 41 f6 f8 00 43 70 0f 9c 41 f6 f9 00 43 70 0f ac +07b0: 41 f6 fa 00 43 70 0f bc 41 f6 fb 00 43 70 0f cc +07c0: 41 f6 fc 00 43 70 0f dc 41 f6 fd 00 43 70 0f ec +07d0: 41 f6 fe 00 43 70 0f fc 41 f6 ff 00 43 70 04 03 +07e0: 41 f6 diff --git a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_s_z_singleton_path_compressed_leaf.snap b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_s_z_singleton_path_compressed_leaf.snap index f872dff20..40fe7abce 100644 --- a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_s_z_singleton_path_compressed_leaf.snap +++ b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_s_z_singleton_path_compressed_leaf.snap @@ -2,4 +2,4 @@ source: src/tree/mirror/alternating/wire_snapshot.rs expression: snap(&n) --- -0000: 01 ab 77 +0000: 01 ab 41 77 41 f6 diff --git a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_s_z_two_child_branch.snap b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_s_z_two_child_branch.snap index 64aadd79e..868ad138b 100644 --- a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_s_z_two_child_branch.snap +++ b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_s_z_two_child_branch.snap @@ -2,4 +2,4 @@ source: src/tree/mirror/alternating/wire_snapshot.rs expression: snap(&n) --- -0000: 00 00 00 00 77 ff 00 72 c0 +0000: 00 00 00 00 41 77 41 f6 ff 00 42 72 c0 41 f6 diff --git a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_z_leaf.snap b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_z_leaf.snap index 5316708d8..20406b198 100644 --- a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_z_leaf.snap +++ b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_z_leaf.snap @@ -2,4 +2,4 @@ source: src/tree/mirror/alternating/wire_snapshot.rs expression: "snap(&leaf(\"a\", 1))" --- -0000: 00 77 +0000: 00 41 77 41 f6 diff --git a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_z_leaf_empty_version.snap b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_z_leaf_empty_version.snap index 959afa5cf..30c63caa0 100644 --- a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_z_leaf_empty_version.snap +++ b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__node_z_leaf_empty_version.snap @@ -2,4 +2,4 @@ source: src/tree/mirror/alternating/wire_snapshot.rs expression: snap(&l) --- -0000: 00 e0 +0000: 00 41 e0 41 f6 diff --git a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__version_empty.snap b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__version_empty.snap index 2c190ec4e..1f0080c24 100644 --- a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__version_empty.snap +++ b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__version_empty.snap @@ -2,4 +2,4 @@ source: src/tree/mirror/alternating/wire_snapshot.rs expression: "snap(&Version::new())" --- -0000: e0 +0000: 41 e0 diff --git a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__version_two_parties_ascending.snap b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__version_two_parties_ascending.snap index 0e8291669..a5dcf90dc 100644 --- a/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__version_two_parties_ascending.snap +++ b/src/tree/mirror/alternating/snapshots/rumors__tree__mirror__alternating__wire_snapshot__version_two_parties_ascending.snap @@ -2,4 +2,4 @@ source: src/tree/mirror/alternating/wire_snapshot.rs expression: snap(&v) --- -0000: 39 6a +0000: 42 39 6a diff --git a/src/tree/mirror/alternating/tests.rs b/src/tree/mirror/alternating/tests.rs index 07acdeb1a..bc4fe2cd9 100644 --- a/src/tree/mirror/alternating/tests.rs +++ b/src/tree/mirror/alternating/tests.rs @@ -3,7 +3,6 @@ use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -use borsh::{BorshDeserialize, BorshSerialize}; use proptest::collection::vec; use proptest::prelude::*; use tokio::runtime::Runtime; @@ -21,6 +20,8 @@ use crate::{Version, message::Message}; use super::{local, mirror, remote}; use crate::tree::mirror::handshake::{self, Intent}; +use serde::Serialize; +use serde::de::DeserializeOwned; // clippy's `missing_const_for_thread_local` misreads `thread_local!`'s // fallback-TLS lowering (illumos among the gate's targets) and denies // initializers that already sit in `const` blocks; the allow keeps @@ -83,7 +84,7 @@ fn mirror_via( scenario: Scenario, ) -> crate::tree::Root where - T: PartialEq + std::fmt::Debug + BorshSerialize + BorshDeserialize + Send + Sync, + T: PartialEq + std::fmt::Debug + Serialize + DeserializeOwned + Send + Sync, { block_on(async move { match scenario { @@ -226,7 +227,7 @@ proptest! { // Tick the party's disjoint clock once per action so every action // carries a strictly-increasing version on that party: inserts take // the first `len` ticks, forgets the ticks after them. - // Each leaf goes to its content-addressed path (as a real insert does), + // Each leaf goes to its version-derived path (as a real insert does), // and a forget targets the path of the insert it cancels — matching how // `redact` reuses the key surfaced by the original insert. let make_actions = |party_index: usize, forgets: &[bool]| -> Vec<_> { @@ -237,7 +238,7 @@ proptest! { for _ in forgets { version.tick(&p); let message = Message::new(()); - let path = Path::for_leaf(&version, message.bytes()); + let path = Path::for_leaf(&version); paths.push(path); actions.push((path, version.clone(), Action::Insert(message))); } diff --git a/src/tree/mirror/alternating/wire_snapshot.rs b/src/tree/mirror/alternating/wire_snapshot.rs index 75c5923b4..4af0e03c1 100644 --- a/src/tree/mirror/alternating/wire_snapshot.rs +++ b/src/tree/mirror/alternating/wire_snapshot.rs @@ -1,15 +1,14 @@ //! Wire-format snapshot tests. //! //! Each type that crosses the protocol boundary is pinned here against an -//! `insta` snapshot of its borsh encoding. A drift means an interop break; +//! `insta` snapshot of its wire encoding. A drift means an interop break; //! re-accept a snapshot only after a deliberate format change. -use borsh::BorshDeserialize; - use super::message; use crate::tree::arb::nth_party; use crate::tree::typed::height::{Height, Root, S, UnderRoot, Z}; use crate::tree::typed::{Children, Hash, Node, Prefix, hash::MERKLE_HASH_LEN}; +use crate::tree::wire; use crate::{Version, message::Message}; /// Map a single-letter party label to its disjoint-party index (see @@ -45,13 +44,13 @@ fn hex_dump(bytes: &[u8]) -> String { s } -fn snap(value: &T) -> String { - hex_dump(&borsh::to_vec(value).unwrap()) +fn snap(value: &T) -> String { + hex_dump(&wire::to_vec(value).unwrap()) } fn prefix_from_bytes(bytes: &[u8]) -> Prefix { assert_eq!(bytes.len(), 32 - H::HEIGHT); - Prefix::::try_from_slice(bytes).expect("known-valid prefix bytes") + wire::from_slice(bytes).expect("known-valid prefix bytes") } fn leaf(party: &str, version: u64) -> Node<(), Z> { diff --git a/src/tree/mirror/party.rs b/src/tree/mirror/party.rs index 119af2e2b..9358b829c 100644 --- a/src/tree/mirror/party.rs +++ b/src/tree/mirror/party.rs @@ -17,9 +17,9 @@ pub(crate) async fn send(party: Party, writer: &mut W) -> Result<(), Error> where W: AsyncWrite + Unpin + ?Sized, { - let mut bytes = Vec::new(); - borsh::BorshSerialize::serialize(&party, &mut bytes)?; - FrameWrite::new(writer).frame(&bytes).await?; + // The frame delimits, so the body is the party's canonical encoding, + // bare. + FrameWrite::new(writer).frame(party.as_bytes()).await?; Ok(()) } @@ -28,10 +28,18 @@ pub(crate) async fn receive(reader: &mut R) -> Result where R: AsyncRead + Unpin + ?Sized, { - use borsh::BorshDeserialize as _; - let bytes = FrameRead::new(reader).frame().await?; - Party::try_from_slice(&bytes).map_err(Error::Io) + Party::decode(&bytes[..]) + .map_err(|e| match e { + // A frame that ends inside the encoding is a truncation, not + // corruption; the reader's own failures pass through. + before::error::Decode::Truncated => { + std::io::Error::new(std::io::ErrorKind::UnexpectedEof, e) + } + before::error::Decode::Io(e) => e, + e => std::io::Error::new(std::io::ErrorKind::InvalidData, e), + }) + .map_err(Error::Io) } #[cfg(test)] diff --git a/src/tree/mirror/party/tests.rs b/src/tree/mirror/party/tests.rs index 23618f2f4..746749af1 100644 --- a/src/tree/mirror/party/tests.rs +++ b/src/tree/mirror/party/tests.rs @@ -32,7 +32,7 @@ fn receive_party(bytes: &[u8]) -> Result { } /// Unwrap the sole error variant this ingress can produce. -fn io_error(result: Result) -> borsh::io::Error { +fn io_error(result: Result) -> std::io::Error { match result { Err(Error::Io(error)) => error, Ok(_) => panic!("a malformed donation must not decode"), @@ -69,7 +69,7 @@ fn truncated_frame_header_is_a_typed_eof() { let error = io_error(receive_party(&vec![0; cut])); assert_eq!( error.kind(), - borsh::io::ErrorKind::UnexpectedEof, + std::io::ErrorKind::UnexpectedEof, "cut after {cut} header bytes must be an unexpected EOF", ); } @@ -86,7 +86,7 @@ fn over_declared_frame_is_a_typed_eof() { bytes.extend_from_slice(&[1, 2, 3, 4]); let error = io_error(receive_party(&bytes)); - assert_eq!(error.kind(), borsh::io::ErrorKind::UnexpectedEof); + assert_eq!(error.kind(), std::io::ErrorKind::UnexpectedEof); } /// A zero-length frame body cannot carry an identity and is a typed error. @@ -98,7 +98,7 @@ fn over_declared_frame_is_a_typed_eof() { #[test] fn empty_frame_body_is_a_typed_error() { let error = io_error(receive_party(&frame(&[]))); - assert_eq!(error.kind(), borsh::io::ErrorKind::UnexpectedEof); + assert_eq!(error.kind(), std::io::ErrorKind::UnexpectedEof); } /// A party encoding cut short inside an honestly sized frame is rejected. @@ -109,11 +109,11 @@ fn empty_frame_body_is_a_typed_error() { /// break party linearity). #[test] fn under_declared_frame_is_a_typed_error() { - let mut body = borsh::to_vec(&nth_party(3)).expect("test parties encode"); + let mut body = nth_party(3).as_bytes().to_vec(); body.truncate(body.len() - 1); let error = io_error(receive_party(&frame(&body))); - assert_eq!(error.kind(), borsh::io::ErrorKind::UnexpectedEof); + assert_eq!(error.kind(), std::io::ErrorKind::UnexpectedEof); } /// A frame with bytes after the party is rejected as non-canonical. @@ -123,11 +123,11 @@ fn under_declared_frame_is_a_typed_error() { /// typed `InvalidData` rather than being silently dropped. #[test] fn trailing_frame_bytes_are_rejected() { - let mut body = borsh::to_vec(&nth_party(3)).expect("test parties encode"); + let mut body = nth_party(3).as_bytes().to_vec(); body.push(0xFF); let error = io_error(receive_party(&frame(&body))); - assert_eq!(error.kind(), borsh::io::ErrorKind::InvalidData); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidData); } /// Receiving one donation consumes exactly its frame, leaving later bytes @@ -166,7 +166,7 @@ proptest! { fn arbitrary_frame_bodies_never_panic(body in vec(any::(), 0..64)) { match receive_party(&frame(&body)) { Ok(party) => { - let reencoded = borsh::to_vec(&party).expect("parties encode"); + let reencoded = party.as_bytes().to_vec(); prop_assert_eq!(reencoded, body, "accepted donation was not canonical"); } Err(Error::Io(_)) => {} diff --git a/src/tree/mirror/streaming/materialized/unknown/tests.rs b/src/tree/mirror/streaming/materialized/unknown/tests.rs index 8e219641f..f60e0ac1a 100644 --- a/src/tree/mirror/streaming/materialized/unknown/tests.rs +++ b/src/tree/mirror/streaming/materialized/unknown/tests.rs @@ -36,7 +36,7 @@ fn tree_and_known(flags_a: &[bool], flags_b: &[bool]) -> (Option`](crate::message::Message). The encoder chunks a supplied -//! subtree's leaves into runs by a byte budget ([`RunBudget`]); once a run's -//! whole body arrives, the frame codec validates its record framing and the -//! incoming adapter decodes each backend-neutral pair exactly once, -//! constructing a backend leaf and validating its content-derived path. +//! one or more leaf records, each itself exact-length-delimited — a CBOR +//! byte string wrapping the [`Version`](crate::Version)'s canonical bytes, +//! then the [`Message`](crate::message::Message)'s CBOR payload. The +//! encoder chunks a supplied subtree's leaves into runs by a byte budget +//! ([`RunBudget`]); once a run's whole body arrives, the frame codec +//! validates its record framing and the incoming adapter decodes each +//! backend-neutral pair exactly once, constructing a backend leaf and +//! validating its version-derived path. //! //! The initiator's distinguished opening question needs no wire frame: its //! content — the initiator's root-fan listing — rides the greeting on the diff --git a/src/tree/mirror/streaming/remote/adapter.rs b/src/tree/mirror/streaming/remote/adapter.rs index 3662f54df..911ba3c30 100644 --- a/src/tree/mirror/streaming/remote/adapter.rs +++ b/src/tree/mirror/streaming/remote/adapter.rs @@ -19,7 +19,7 @@ //! the listed child radices. `Match` and `Query` reactions consume those radices //! positionally; a nested `Query` thereby creates the lower scope which will //! interpret its future reply. `Supply` does not consume the positional cursor: -//! its content-derived path recovers its child radix independently. The +//! its version-derived path recovers its child radix independently. The //! leaf-height exception is an empty `Query`: it consumes its leaf position and //! requests that leaf itself, creating a terminal scope at the same height //! rather than descending. The initiator's opening reply is the sole diff --git a/src/tree/mirror/streaming/remote/adapter/decode.rs b/src/tree/mirror/streaming/remote/adapter/decode.rs index bae4e7192..d08d55011 100644 --- a/src/tree/mirror/streaming/remote/adapter/decode.rs +++ b/src/tree/mirror/streaming/remote/adapter/decode.rs @@ -27,6 +27,7 @@ use super::{ scope::Scope, }; +use serde::de::DeserializeOwned; /// One reconstructed reply and any questions it asks next. pub struct Decoded where @@ -64,7 +65,7 @@ where /// /// The wire shape is one supplies-only reply — empty when deletion pruning /// left nothing to ship — whose leaf records group into height-`G` -/// subtrees by their content-derived paths under `parent`, followed by the +/// subtrees by their version-derived paths under `parent`, followed by the /// stream end. Unlike [`decode_reply`], which materializes one whole reply /// before yielding it, this stream yields each assembled node as soon as /// its group completes: the consumer pairs supplies with the responder's @@ -85,7 +86,7 @@ pub fn early_supplies( ) -> impl Stream), DecodeError>> + Send where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, G: Convert, S: Height, F: Stream> + Unpin + Send + 'static, @@ -150,7 +151,7 @@ async fn read_early( ) -> Result<(), DecodeError> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, G: Height, S: Height, F: Stream> + Unpin, @@ -166,8 +167,7 @@ where any = true; for record in records.records() { let (version, message) = record.map_err(DecodeError::Record)?; - let (leaf_prefix, _) = - supplies.observe::(parent, &version, &message)?; + let (leaf_prefix, _) = supplies.observe::(parent, &version)?; // The set-length half of the greeting's priced // premises, charged per record before the payload // takes backend custody: a peer supplying past its @@ -220,7 +220,7 @@ pub async fn decode_reply( ) -> Result, Vec>>, DecodeError> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, H: Height, S: Convert, S>: Height, @@ -250,7 +250,7 @@ pub async fn decode_leaf_reply( ) -> Result>>, DecodeError> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, F: Stream> + Unpin, { decode( @@ -280,7 +280,7 @@ async fn decode( ) -> Result>, DecodeError> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, H: Convert, S: Height, F: Stream> + Unpin, @@ -324,7 +324,7 @@ async fn read_reply( ) -> Result>, DecodeError> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, H: Height, S: Height, F: Stream> + Unpin, @@ -371,9 +371,9 @@ where // decoded vector of leaves. for record in records.records() { let (version, message) = record.map_err(DecodeError::Record)?; - let (leaf_prefix, run) = - read.supplies - .observe::(scope.parent(), &version, &message)?; + let (leaf_prefix, run) = read + .supplies + .observe::(scope.parent(), &version)?; if let Some((radix, prefix)) = run { read.skeleton.push(Skeleton::Supply { radix, prefix }); } @@ -445,7 +445,7 @@ where .expect("each supplied run assembles to exactly one node"); assert_eq!( actual, prefix, - "assembly preserves the content-derived supplied prefix", + "assembly preserves the version-derived supplied prefix", ); ProtocolReaction::Supply(radix, node) } @@ -498,14 +498,12 @@ impl SupplyRuns { } /// Validate one supplied leaf and identify the start of a new run. - fn observe( + fn observe( &mut self, expected_parent: Prefix>, version: &crate::Version, - message: &crate::message::Message, ) -> Result<(Prefix, Option<(u8, Prefix)>), DecodeError> where - T: Send + Sync + 'static, S: Height, { // The declared aggregate covers every version the peer's tree @@ -520,7 +518,7 @@ impl SupplyRuns { actual, }); } - let path = Path::for_leaf(version, message.as_slice()); + let path = Path::for_leaf(version); let leaf_prefix = Prefix::::containing(&path); let node_prefix = Prefix::::containing(&path); let (parent, radix) = node_prefix.pop(); diff --git a/src/tree/mirror/streaming/remote/adapter/error.rs b/src/tree/mirror/streaming/remote/adapter/error.rs index 8b7130362..6c85b549e 100644 --- a/src/tree/mirror/streaming/remote/adapter/error.rs +++ b/src/tree/mirror/streaming/remote/adapter/error.rs @@ -69,10 +69,10 @@ pub enum DecodeError { /// Transport control leaked through the demultiplexer into reply decoding. #[error("a stream-end control reached the protocol reply decoder")] UnexpectedStreamEnd, - /// A supplied leaf's content-derived path is outside the expected scope. + /// A supplied leaf's version-derived path is outside the expected scope. #[error("supplied leaf {actual:02x?} is outside reply scope {expected:02x?}")] LeafOutsideScope { expected: Vec, actual: [u8; 32] }, - /// Supplied leaves were not strictly ascending by content-derived path. + /// Supplied leaves were not strictly ascending by version-derived path. #[error("supplied leaf {current:02x?} does not follow {previous:02x?}")] LeafOrder { previous: [u8; 32], diff --git a/src/tree/mirror/streaming/remote/adapter/tests.rs b/src/tree/mirror/streaming/remote/adapter/tests.rs index 2dc9aceab..f94c3296b 100644 --- a/src/tree/mirror/streaming/remote/adapter/tests.rs +++ b/src/tree/mirror/streaming/remote/adapter/tests.rs @@ -62,15 +62,19 @@ struct LeafCase { } impl LeafCase { + /// A deterministic test leaf: the version scalar folds `value` and + /// `ticks` together so distinct cases mint distinct versions — the + /// axis paths derive from — while `value` also picks the payload. fn new(value: u64, ticks: u8) -> Self { Self { value, - version: Version::try_from(u64::from(ticks)).expect("u8 is a valid linear version"), + version: Version::try_from(value.wrapping_shl(8) | u64::from(ticks)) + .expect("every u64 scalar is a valid linear version"), message: Message::new(value), } } fn path(&self) -> Path { - Path::for_leaf(&self.version, self.message.as_slice()) + Path::for_leaf(&self.version) } } diff --git a/src/tree/mirror/streaming/remote/adapter/tests/fan_occupancy.rs b/src/tree/mirror/streaming/remote/adapter/tests/fan_occupancy.rs index 92d05cd7d..f740d6c04 100644 --- a/src/tree/mirror/streaming/remote/adapter/tests/fan_occupancy.rs +++ b/src/tree/mirror/streaming/remote/adapter/tests/fan_occupancy.rs @@ -45,12 +45,11 @@ const PER_FRAME: usize = 16; fn leaves(count: u64) -> Vec<(Version, Message)> { let mut leaves: Vec<(Version, Message)> = (0..count) .map(|index| { - let version = - Version::try_from(index % 200 + 1).expect("small linear versions are valid"); + let version = Version::try_from(index + 1).expect("small linear versions are valid"); (version, Message::new(index)) }) .collect(); - leaves.sort_by_key(|(version, message)| Path::for_leaf(version, message.as_slice())); + leaves.sort_by_key(|(version, _)| Path::for_leaf(version)); leaves } diff --git a/src/tree/mirror/streaming/remote/adapter/tests/malformed.rs b/src/tree/mirror/streaming/remote/adapter/tests/malformed.rs index 53cf00e5c..cc213e333 100644 --- a/src/tree/mirror/streaming/remote/adapter/tests/malformed.rs +++ b/src/tree/mirror/streaming/remote/adapter/tests/malformed.rs @@ -31,7 +31,7 @@ use crate::tree::mirror::streaming::remote::codec::{ /// A nonempty reply must end on its last reaction; a later bare end is ambiguous and invalid. #[test] fn bare_end_cannot_follow_reactions() { - let path = Path::for_leaf(&Version::new(), &[0]); + let path = Path::for_leaf(&Version::new()); let parent = Prefix::>::containing(&path); let frames: Vec> = vec![ Frame::Reaction(WireReaction::Match, Flow::Continue), @@ -57,7 +57,7 @@ fn bare_end_cannot_follow_reactions() { /// Exhausting the frame stream without an explicit boundary reports truncation, not a reply. #[test] fn stream_exhaustion_before_a_boundary_is_truncation() { - let path = Path::for_leaf(&Version::new(), &[0]); + let path = Path::for_leaf(&Version::new()); let parent = Prefix::>::containing(&path); let mut frames = stream::iter([Frame::<()>::Reaction(WireReaction::Match, Flow::Continue)]); @@ -84,7 +84,7 @@ fn stream_exhaustion_before_a_boundary_is_truncation() { /// past the fan — rather than after the whole reply decodes. #[test] fn an_unpositioned_match_is_rejected_in_both_directions() { - let path = Path::for_leaf(&Version::new(), &[0]); + let path = Path::for_leaf(&Version::new()); let parent = Prefix::>>::containing(&path); // One listed child admits one positional reaction; the second Match // must fail at its own frame with the reply still unterminated. @@ -135,7 +135,7 @@ fn an_unpositioned_match_is_rejected_in_both_directions() { /// Prefix-free queries require a remaining positional child in both conversion directions. #[test] fn an_unpositioned_query_is_rejected_in_both_directions() { - let path = Path::for_leaf(&Version::new(), &[0]); + let path = Path::for_leaf(&Version::new()); let parent = Prefix::>>::containing(&path); let listing = vec![(1, hash(1))]; let frames: Vec> = vec![Frame::Reaction( @@ -180,7 +180,7 @@ fn an_unpositioned_query_is_rejected_in_both_directions() { /// All eight leaf-query paths pin validity, error precedence, framing, and publication. #[test] fn leaf_query_matrix_is_exhaustive() { - let path = Path::for_leaf(&Version::new(), &[0]); + let path = Path::for_leaf(&Version::new()); let parent = Prefix::>::containing(&path); let radix = 3; let mut checked = 0; @@ -271,7 +271,7 @@ fn leaf_query_matrix_is_exhaustive() { /// Transport stream-end control is rejected if it leaks past demultiplexing. #[test] fn stream_end_is_not_a_protocol_reply() { - let path = Path::for_leaf(&Version::new(), &[0]); + let path = Path::for_leaf(&Version::new()); let parent = Prefix::>::containing(&path); let mut frames = stream::iter([Frame::<()>::End(End::Stream)]); @@ -319,7 +319,7 @@ fn under_root_pair() -> [(Version, Message, Path); 2] { unreachable!("the finite radix alphabet forces a collision") } -/// Consecutive leaves in one content-derived run assemble as one node and reexplode exactly. +/// Consecutive leaves in one version-derived run assemble as one node and reexplode exactly. #[test] fn a_multi_leaf_run_is_one_supplied_subtree() { let leaves = under_root_pair(); @@ -486,7 +486,7 @@ fn a_zero_length_record_fails_as_a_version_decode_error() { let DecodeError::Record(DecodeLeafError::Version(source)) = error else { panic!("expected a version decode error, got {error:?}"); }; - assert_eq!(source.kind(), borsh::io::ErrorKind::UnexpectedEof); + assert_eq!(source.kind(), std::io::ErrorKind::UnexpectedEof); } /// The declared version bound admits exactly the versions it covers. diff --git a/src/tree/mirror/streaming/remote/adapter/tests/opening.rs b/src/tree/mirror/streaming/remote/adapter/tests/opening.rs index c7858b68d..db221aed8 100644 --- a/src/tree/mirror/streaming/remote/adapter/tests/opening.rs +++ b/src/tree/mirror/streaming/remote/adapter/tests/opening.rs @@ -109,11 +109,11 @@ fn empty_listing_replays_the_empty_opening() { } /// The opening-supply reply decodes into whole root children, one per -/// content-derived radix group, in ascending radix order. +/// version-derived radix group, in ascending radix order. #[test] fn opening_supplies_decode_by_radix_group() { // Enough cases that at least two distinct first bytes exist; the - // content-derived paths pick the grouping. + // version-derived paths pick the grouping. let mut cases: Vec = (0..6).map(|i| LeafCase::new(1_000 + i, 1)).collect(); cases.sort_by_key(LeafCase::path); let first_byte = |case: &LeafCase| <[u8; 32]>::from(case.path())[0]; diff --git a/src/tree/mirror/streaming/remote/adapter/tests/parking.rs b/src/tree/mirror/streaming/remote/adapter/tests/parking.rs index 0339a482c..f79f58290 100644 --- a/src/tree/mirror/streaming/remote/adapter/tests/parking.rs +++ b/src/tree/mirror/streaming/remote/adapter/tests/parking.rs @@ -84,7 +84,7 @@ fn parked_supply_reply_holds_handles_not_subtrees() { .clone() .expect("a populated tree has a root node"); - // The real root fan: content-addressed leaves scatter across first + // The real root fan: version-addressed leaves scatter across first // bytes, so the fan is wide and its children are multi-leaf. let children: Vec<(u8, typed::Node)> = root.into_children().into_iter().collect(); diff --git a/src/tree/mirror/streaming/remote/adapter/tests/properties.rs b/src/tree/mirror/streaming/remote/adapter/tests/properties.rs index 9231d7b48..11d793353 100644 --- a/src/tree/mirror/streaming/remote/adapter/tests/properties.rs +++ b/src/tree/mirror/streaming/remote/adapter/tests/properties.rs @@ -958,7 +958,7 @@ where .map(|offset| LeafCase::new(leaf.value.wrapping_add(u64::from(offset)), 0)) .map(|candidate| Prefix::>::containing(&candidate.path())) .find(|candidate| *candidate != actual) - .expect("a non-root prefix has another content-derived value") + .expect("a non-root prefix has another version-derived value") } fn assert_foreign_error( @@ -1077,7 +1077,7 @@ proptest! { } } - /// At every height, a content-derived supply remains correctly keyed when + /// At every height, a version-derived supply remains correctly keyed when /// merge-ordered among arbitrary positional matches and queries. #[test] fn mixed_reactions_are_lossless_at_every_height( @@ -1120,7 +1120,7 @@ proptest! { } /// At every height with more than one possible parent scope, a supplied - /// leaf is rejected unless its content-derived path is under that scope. + /// leaf is rejected unless its version-derived path is under that scope. #[test] fn foreign_supply_is_rejected_at_every_scopable_height( value in any::(), diff --git a/src/tree/mirror/streaming/remote/codec.rs b/src/tree/mirror/streaming/remote/codec.rs index 975729f46..154c2f4db 100644 --- a/src/tree/mirror/streaming/remote/codec.rs +++ b/src/tree/mirror/streaming/remote/codec.rs @@ -23,7 +23,7 @@ //! exact `u32` record length. The codec validates the run's record framing //! once its whole body arrives but leaves the records encoded; the adapter //! decodes them one at a time, constructs its backend-specific leaves, and -//! validates their content-addressed paths. How many records share one run +//! validates their version-derived paths. How many records share one run //! is the sender's choice within the session's [`RunBudget`], and the //! decoder holds arriving frames to that same budget: any within-budget //! batching decodes, a single record of any size decodes (the encoder's diff --git a/src/tree/mirror/streaming/remote/codec/capture.rs b/src/tree/mirror/streaming/remote/codec/capture.rs index 1273ecbfd..4f9c2547c 100644 --- a/src/tree/mirror/streaming/remote/codec/capture.rs +++ b/src/tree/mirror/streaming/remote/codec/capture.rs @@ -14,8 +14,6 @@ use std::{collections::BTreeMap, fmt::Write as _}; -use borsh::BorshDeserialize; - use crate::Version; use crate::tree::mirror::framing::{GREETING_SIZE_WORDS_LEN, LENGTH_HEADER_LEN, greeting_words}; use crate::tree::mirror::streaming::message::initiates; @@ -161,7 +159,7 @@ impl Control { greeting_words(&version_frame[LENGTH_HEADER_LEN..]) .expect("captured version frame carries its three size words"); let version = - Version::try_from_slice(&version_frame[LENGTH_HEADER_LEN + GREETING_SIZE_WORDS_LEN..]) + Version::decode(&version_frame[LENGTH_HEADER_LEN + GREETING_SIZE_WORDS_LEN..]) .expect("captured version frame is canonical"); // The greeting always carries its listing frame directly behind the // version frame (empty tree = empty listing, still framed). @@ -337,7 +335,7 @@ fn supply_lines(run: Vec) -> Vec { let mut lines = vec![format!("supply run: {} record(s)", run.record_count())]; for (index, record) in run.record_slices().enumerate() { let mut input = record; - match Version::deserialize(&mut input) { + match ciborium::de::from_reader::(&mut input) { Ok(version) => lines.push(format!( " record {index}: version {version}, message {} byte(s)", input.len(), @@ -351,21 +349,30 @@ fn supply_lines(run: Vec) -> Vec { } /// Render one root-fan listing frame's children, or its explicit decode -/// failure: the listing is peer-controlled borsh, so the renderer must +/// failure: the listing is peer-controlled bytes, so the renderer must /// never present undecodable bytes as a quietly hex-only frame. /// /// The canonical child order is held by the codec's own /// `validate_children`, the same rule the handshake applies before /// building scope from a received listing. fn listing_lines(body: &[u8]) -> Vec { - let children = match >::try_from_slice(body) { - Ok(children) => children, - Err(err) => { - return vec![format!( - "listing undecodable ({err}); the exact bytes stand below" - )]; - } - }; + const RECORD: usize = 1 + crate::tree::typed::hash::MERKLE_HASH_LEN; + if !body.len().is_multiple_of(RECORD) { + return vec![format!( + "listing undecodable ({} bytes is not a whole number of radix-hash records); \ + the exact bytes stand below", + body.len() + )]; + } + let children: Vec<(u8, Hash)> = body + .chunks_exact(RECORD) + .map(|record| { + let (&radix, hash) = record.split_first().expect("a record has a radix byte"); + let mut bytes = [0u8; crate::tree::typed::hash::MERKLE_HASH_LEN]; + bytes.copy_from_slice(hash); + (radix, Hash(bytes)) + }) + .collect(); if let Err(err) = validate_children(&children) { return vec![format!( "listing not canonical ({err}); the exact bytes stand below" diff --git a/src/tree/mirror/streaming/remote/codec/capture/tests.rs b/src/tree/mirror/streaming/remote/codec/capture/tests.rs index a461ae53a..35b2fb32b 100644 --- a/src/tree/mirror/streaming/remote/codec/capture/tests.rs +++ b/src/tree/mirror/streaming/remote/codec/capture/tests.rs @@ -13,12 +13,22 @@ use crate::tree::typed::hash::MERKLE_HASH_LEN; use super::super::frame::LeafRun; -/// The committed fixture pair: two supply runs identical except one -/// record's version decode to renderings differing at exactly the line -/// naming that record. +/// Encode a listing as its wire form: raw radix-hash records. +fn encode_listing(children: &[(u8, Hash)]) -> Vec { + let mut body = Vec::new(); + for (radix, hash) in children { + body.push(*radix); + body.extend_from_slice(hash.as_bytes()); + } + body +} + +/// The capture renderer decodes each supply record structurally. /// -/// That one-line diff is the field-level account an insta re-accept -/// shows beside the hex. +/// Two runs differing only in the record's version render record lines +/// that differ exactly at the line naming that record, with the record +/// count and the (identical) payload accounting unchanged: the +/// field-level account an insta re-accept shows beside the hex. #[test] fn supply_decode_names_the_field_that_moved() { let party = before::Party::seed(); @@ -41,9 +51,9 @@ fn supply_decode_names_the_field_that_moved() { assert!(a[1].contains(&format!("version {low}"))); assert!(b[1].contains(&format!("version {high}"))); // The message is identical on both sides, so both record lines - // account it identically: eight borsh bytes of the same u64. - assert!(a[1].ends_with("message 8 byte(s)")); - assert!(b[1].ends_with("message 8 byte(s)")); + // account it identically: one CBOR byte for the small u64. + assert!(a[1].ends_with("message 1 byte(s)")); + assert!(b[1].ends_with("message 1 byte(s)")); } /// Unparseable supply payloads render an explicit decode failure, never @@ -83,7 +93,7 @@ fn listing_decodes_children_and_convicts_garbage() { (0x3_u8, Hash([0xab; MERKLE_HASH_LEN])), (0xc_u8, Hash([0x01; MERKLE_HASH_LEN])), ]; - let body = borsh::to_vec(&children).expect("a listing serializes"); + let body = encode_listing(&children); let lines = listing_lines(&body); assert_eq!(lines[0], "listing: 2 child(ren)"); assert_eq!( @@ -152,7 +162,7 @@ fn non_canonical_listing_renders_failure_not_silent_hex() { (0xc_u8, Hash([0x01; MERKLE_HASH_LEN])), (0x3_u8, Hash([0xab; MERKLE_HASH_LEN])), ]; - let body = borsh::to_vec(&children).expect("a listing serializes"); + let body = encode_listing(&children); let lines = listing_lines(&body); assert_eq!(lines.len(), 1); assert!( diff --git a/src/tree/mirror/streaming/remote/codec/decode.rs b/src/tree/mirror/streaming/remote/codec/decode.rs index 4565b4ef5..f6ae3ea1e 100644 --- a/src/tree/mirror/streaming/remote/codec/decode.rs +++ b/src/tree/mirror/streaming/remote/codec/decode.rs @@ -4,9 +4,7 @@ use std::slice; #[cfg(test)] -use borsh::BorshDeserialize; -#[cfg(test)] -use borsh::io::{ErrorKind, Read}; +use std::io::{ErrorKind, Read}; use crate::tree::mirror::framing::LENGTH_HEADER_LEN; use crate::tree::typed::{Hash, hash::MERKLE_HASH_LEN}; @@ -28,9 +26,12 @@ use super::{ signal::{Signal, Speaker, Stream, WireSignal}, }; +#[cfg(test)] +use serde::de::DeserializeOwned; + /// Decode one frame from `read`, leaving subsequent bytes untouched. #[cfg(test)] -pub fn decode( +pub fn decode( speaker: Speaker, budget: RunBudget, read: &mut impl Read, @@ -40,7 +41,7 @@ pub fn decode( /// Decode exactly one frame from a slice, rejecting bytes after it. #[cfg(test)] -pub fn decode_exact( +pub fn decode_exact( speaker: Speaker, budget: RunBudget, input: &[u8], @@ -77,7 +78,7 @@ impl<'a, R: Read> FrameDecoder<'a, R> { } } - fn decode(mut self) -> Result, DecodeError> { + fn decode(mut self) -> Result, DecodeError> { let (stream, signal) = self.signal()?; let frame = self .body(signal) @@ -92,7 +93,7 @@ impl<'a, R: Read> FrameDecoder<'a, R> { decode_signal(self.speaker, byte) } - fn body(&mut self, signal: Signal) -> Result, DecodeErrorKind> { + fn body(&mut self, signal: Signal) -> Result, DecodeErrorKind> { let frame = match signal { Signal::Match(flow) => Frame::Reaction(Reaction::Match, flow), Signal::QueryEmpty(flow) => Frame::Reaction(Reaction::Query(Vec::new()), flow), diff --git a/src/tree/mirror/streaming/remote/codec/decode/async_io.rs b/src/tree/mirror/streaming/remote/codec/decode/async_io.rs index a6be35424..4a8866228 100644 --- a/src/tree/mirror/streaming/remote/codec/decode/async_io.rs +++ b/src/tree/mirror/streaming/remote/codec/decode/async_io.rs @@ -2,7 +2,7 @@ use std::slice; -use borsh::{BorshDeserialize, io::ErrorKind}; +use std::io::ErrorKind; use tokio::io::{AsyncRead, AsyncReadExt}; use super::super::{ @@ -17,6 +17,7 @@ use crate::tree::{ typed::Hash, }; +use serde::de::DeserializeOwned; /// Async frame reader over one speaker's transport direction. /// /// EOF before a signal is a clean direction close and returns `None`. Once a @@ -62,7 +63,7 @@ impl FrameRead { /// as a signal. Either retain the in-flight future across polls until /// it resolves, or read nothing further from this direction after a /// cancellation. - pub async fn frame( + pub async fn frame( &mut self, ) -> Result>, DecodeError> { let Some((stream, signal)) = read_signal(self.speaker, &mut self.read).await? else { @@ -107,7 +108,7 @@ impl<'a, R: AsyncRead + Unpin> AsyncFrameDecoder<'a, R> { Self { read, budget } } - async fn body( + async fn body( &mut self, signal: Signal, ) -> Result, DecodeErrorKind> { @@ -193,7 +194,7 @@ impl<'a, R: AsyncRead + Unpin> AsyncFrameDecoder<'a, R> { /// Type an I/O failure by the frame part it interrupted: end-of-stream is a /// contextual truncation, anything else a plain read failure. -fn classify(part: FramePart, source: borsh::io::Error) -> DecodeErrorKind { +fn classify(part: FramePart, source: std::io::Error) -> DecodeErrorKind { match source.kind() { ErrorKind::UnexpectedEof => DecodeErrorKind::Truncated { missing: part, diff --git a/src/tree/mirror/streaming/remote/codec/decode/tests.rs b/src/tree/mirror/streaming/remote/codec/decode/tests.rs index 34bba1c15..72289053f 100644 --- a/src/tree/mirror/streaming/remote/codec/decode/tests.rs +++ b/src/tree/mirror/streaming/remote/codec/decode/tests.rs @@ -1,4 +1,3 @@ -use borsh::BorshSerialize; use proptest::prelude::*; use super::*; @@ -14,8 +13,9 @@ use super::super::{ const SPEAKERS: [Speaker; 2] = [Speaker::Initiator, Speaker::Responder]; -/// A one-byte prefix of a Version whose gamma integer is incomplete. -const TRUNCATED_VERSION: &[u8] = &[1]; +/// A CBOR byte-string header promising two version bytes, cut short after +/// one: the version field ends inside its own framing. +const TRUNCATED_VERSION: &[u8] = &[0x42, 0x01]; fn stream(index: u8) -> Stream { Stream::new(index).unwrap() @@ -34,11 +34,12 @@ fn supply(stream: Stream, flow: Flow, body: &[u8]) -> Vec { encoded } -/// One length-prefixed leaf record as it appears inside a run body. +/// One length-prefixed leaf record as it appears inside a run body: the +/// version as one CBOR value, then the payload's CBOR bytes bare. fn record(version: &Version, message: &Message) -> Vec { let mut body = Vec::new(); - version.serialize(&mut body).unwrap(); - message.serialize(&mut body).unwrap(); + ciborium::ser::into_writer(version, &mut body).unwrap(); + body.extend_from_slice(message.as_slice()); let mut record = (body.len() as u32).to_be_bytes().to_vec(); record.extend_from_slice(&body); record @@ -124,7 +125,7 @@ fn truncated_bodies_are_rejected() { panic!("unexpected error kind"); }; assert_eq!(actual, missing); - assert_eq!(source.kind(), borsh::io::ErrorKind::UnexpectedEof); + assert_eq!(source.kind(), std::io::ErrorKind::UnexpectedEof); } } } @@ -228,12 +229,12 @@ fn a_zero_length_record_is_structurally_valid() { let DecodeLeafError::Version(source) = error else { panic!("unexpected record error"); }; - assert_eq!(source.kind(), borsh::io::ErrorKind::UnexpectedEof); + assert_eq!(source.kind(), std::io::ErrorKind::UnexpectedEof); } } /// A record's canonical decoding is deferred to the run's record iterator, -/// which types each failure and retains the Borsh source error. +/// which types each failure and retains the source error. #[test] fn supplied_record_errors_are_typed() { let mut truncated_version = (TRUNCATED_VERSION.len() as u32).to_be_bytes().to_vec(); @@ -243,10 +244,10 @@ fn supplied_record_errors_are_typed() { let DecodeLeafError::Version(source) = error else { panic!("unexpected record error"); }; - assert_eq!(source.kind(), borsh::io::ErrorKind::UnexpectedEof); + assert_eq!(source.kind(), std::io::ErrorKind::UnexpectedEof); let mut version = Vec::new(); - Version::new().serialize(&mut version).unwrap(); + ciborium::ser::into_writer(&Version::new(), &mut version).unwrap(); let mut missing_message = (version.len() as u32).to_be_bytes().to_vec(); missing_message.extend_from_slice(&version); let run = LeafRun::::from_encoded(missing_message).unwrap(); @@ -254,9 +255,9 @@ fn supplied_record_errors_are_typed() { let DecodeLeafError::Message(source) = error else { panic!("unexpected record error"); }; - assert_eq!(source.kind(), borsh::io::ErrorKind::InvalidData); + assert_eq!(source.kind(), std::io::ErrorKind::UnexpectedEof); - 0_u64.serialize(&mut version).unwrap(); + ciborium::ser::into_writer(&0_u64, &mut version).unwrap(); version.push(u8::MIN); let mut trailing = (version.len() as u32).to_be_bytes().to_vec(); trailing.extend_from_slice(&version); @@ -367,7 +368,7 @@ fn async_eof_distinguishes_close_from_truncation() { DecodeErrorKind::Truncated { missing: actual, source, - } if actual == missing && source.kind() == borsh::io::ErrorKind::UnexpectedEof + } if actual == missing && source.kind() == std::io::ErrorKind::UnexpectedEof )); } } @@ -418,9 +419,9 @@ fn async_invalid_signal_does_not_consume_a_body() { struct FailingReader; -impl borsh::io::Read for FailingReader { - fn read(&mut self, _buf: &mut [u8]) -> borsh::io::Result { - Err(borsh::io::ErrorKind::Other.into()) +impl std::io::Read for FailingReader { + fn read(&mut self, _buf: &mut [u8]) -> std::io::Result { + Err(std::io::ErrorKind::Other.into()) } } @@ -435,7 +436,7 @@ fn reader_errors_are_contextual() { DecodeErrorKind::Read { part: FramePart::Signal, source, - } if source.kind() == borsh::io::ErrorKind::Other + } if source.kind() == std::io::ErrorKind::Other )); } } @@ -466,7 +467,7 @@ fn supply_truncation_at_chunk_boundaries_is_typed() { DecodeErrorKind::Truncated { missing: FramePart::SupplyRun, ref source, - } if source.kind() == borsh::io::ErrorKind::UnexpectedEof + } if source.kind() == std::io::ErrorKind::UnexpectedEof ), "cut after {delivered} delivered body bytes" ); diff --git a/src/tree/mirror/streaming/remote/codec/encode.rs b/src/tree/mirror/streaming/remote/codec/encode.rs index 5e60fa407..8bc985404 100644 --- a/src/tree/mirror/streaming/remote/codec/encode.rs +++ b/src/tree/mirror/streaming/remote/codec/encode.rs @@ -1,7 +1,7 @@ //! Canonical frame encoding. #[cfg(test)] -use borsh::io::Write; +use std::io::Write; use crate::tree::{ mirror::framing::{LENGTH_HEADER_LEN, length_header}, diff --git a/src/tree/mirror/streaming/remote/codec/encode/tests.rs b/src/tree/mirror/streaming/remote/codec/encode/tests.rs index 5e645406e..8a3bd85cc 100644 --- a/src/tree/mirror/streaming/remote/codec/encode/tests.rs +++ b/src/tree/mirror/streaming/remote/codec/encode/tests.rs @@ -1,4 +1,3 @@ -use borsh::BorshSerialize; use proptest::prelude::*; use std::{ pin::Pin, @@ -125,8 +124,8 @@ proptest! { let message = Message::new(*value); run.push(version, &message).unwrap(); let mut record = Vec::new(); - version.serialize(&mut record).unwrap(); - message.serialize(&mut record).unwrap(); + ciborium::ser::into_writer(version, &mut record).unwrap(); + record.extend_from_slice(message.as_slice()); body.extend_from_slice(&(record.len() as u32).to_be_bytes()); body.extend_from_slice(&record); } @@ -144,12 +143,12 @@ proptest! { struct FailingWriter; -impl borsh::io::Write for FailingWriter { - fn write(&mut self, _buf: &[u8]) -> borsh::io::Result { - Err(borsh::io::ErrorKind::Other.into()) +impl std::io::Write for FailingWriter { + fn write(&mut self, _buf: &[u8]) -> std::io::Result { + Err(std::io::ErrorKind::Other.into()) } - fn flush(&mut self) -> borsh::io::Result<()> { + fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } @@ -167,7 +166,7 @@ fn writer_errors_are_contextual() { EncodeErrorKind::Write { part: FramePart::Signal, source, - } if source.kind() == borsh::io::ErrorKind::Other + } if source.kind() == std::io::ErrorKind::Other )); } } @@ -218,7 +217,7 @@ fn async_writer_errors_are_contextual() { EncodeErrorKind::Write { part: FramePart::Signal, source, - } if source.kind() == borsh::io::ErrorKind::Other + } if source.kind() == std::io::ErrorKind::Other )); let mut writer = FrameWrite::new(speaker, FailingAsyncWriter(AsyncFailure::Flush)); @@ -227,7 +226,7 @@ fn async_writer_errors_are_contextual() { assert!(matches!( error.kind, EncodeErrorKind::Flush(source) - if source.kind() == borsh::io::ErrorKind::Other + if source.kind() == std::io::ErrorKind::Other )); } } diff --git a/src/tree/mirror/streaming/remote/codec/error.rs b/src/tree/mirror/streaming/remote/codec/error.rs index d4f930616..eb9befe91 100644 --- a/src/tree/mirror/streaming/remote/codec/error.rs +++ b/src/tree/mirror/streaming/remote/codec/error.rs @@ -67,10 +67,10 @@ pub enum EncodeErrorKind { Write { part: FramePart, #[source] - source: borsh::io::Error, + source: std::io::Error, }, #[error("could not flush the completed frame")] - Flush(#[source] borsh::io::Error), + Flush(#[source] std::io::Error), #[error(transparent)] SupplyTooLarge(#[from] LengthOverflow), } @@ -94,7 +94,7 @@ impl EncodeError { } } -/// A Borsh or canonicality failure while decoding a supplied leaf record. +/// A decode or canonicality failure in a supplied leaf record. /// /// Produced by the run's record iterator (`LeafRun::records`), which the /// incoming adapter drives record by record; run *structure* is instead @@ -103,9 +103,9 @@ impl EncodeError { #[derive(Debug, thiserror::Error)] pub enum DecodeLeafError { #[error("supplied Version could not be decoded")] - Version(#[source] borsh::io::Error), + Version(#[source] std::io::Error), #[error("supplied Message could not be decoded")] - Message(#[source] borsh::io::Error), + Message(#[source] std::io::Error), #[error("{count} trailing bytes follow the supplied Version and Message")] TrailingBytes { count: usize }, } @@ -117,7 +117,7 @@ pub enum DecodeErrorKind { Read { part: FramePart, #[source] - source: borsh::io::Error, + source: std::io::Error, }, #[error(transparent)] InvalidSignal(#[from] DecodeSignalError), @@ -125,7 +125,7 @@ pub enum DecodeErrorKind { Truncated { missing: FramePart, #[source] - source: borsh::io::Error, + source: std::io::Error, }, #[error(transparent)] QueryOutOfOrder(#[from] QueryOrderError), diff --git a/src/tree/mirror/streaming/remote/codec/frame.rs b/src/tree/mirror/streaming/remote/codec/frame.rs index c29a62783..712f6c46c 100644 --- a/src/tree/mirror/streaming/remote/codec/frame.rs +++ b/src/tree/mirror/streaming/remote/codec/frame.rs @@ -2,8 +2,6 @@ use std::marker::PhantomData; -use borsh::BorshDeserialize; - use crate::{ Version, message::Message, @@ -16,6 +14,7 @@ use crate::{ use super::error::{DecodeLeafError, QueryOrderError}; use super::signal::{End, Flow, Stream}; +use serde::de::DeserializeOwned; /// The count byte stores one less than the nonempty query's actual fan. pub const QUERY_COUNT_BIAS: usize = 1; @@ -55,8 +54,11 @@ pub type WireFrame = (Stream, Frame); /// /// A run is a delimited sequence of one or more `(Version, Message)` /// records: each record is a [`LENGTH_HEADER_LEN`]-byte big-endian length -/// followed by the canonical encodings of its version and message, back to -/// back. The run stays encoded on both sides of the wire — the encoder +/// followed by one CBOR value (a byte string wrapping the version's +/// canonical encoding) and then the message's CBOR payload, back to back — +/// the record header delimits the payload, so it travels bare, and the +/// version's CBOR framing is what lets the decoder split the two without +/// re-measuring. The run stays encoded on both sides of the wire — the encoder /// appends records copied from borrowed leaf data ([`push`](Self::push)) and /// the decoder yields them one at a time ([`records`](Self::records)) — so /// neither side materializes a decoded vector of leaves per frame; the bound @@ -134,12 +136,17 @@ impl LeafRun { /// Bytes one record with these components will occupy in a run. /// - /// Saturating: a sum past `usize::MAX` cannot occur for in-memory - /// slices, and an over-large record is rejected by [`push`](Self::push) - /// regardless. + /// Exactly what [`push`](Self::push) writes — the record header, the + /// version's CBOR byte-string framing plus its canonical bytes, and + /// the payload — pinned against an actual push by + /// `record_len_matches_an_actual_push`. Saturating: a sum past + /// `usize::MAX` cannot occur for in-memory slices, and an over-large + /// record is rejected by [`push`](Self::push) regardless. pub fn record_len(version: &Version, message: &Message) -> usize { + let version = version.as_bytes().len(); LENGTH_HEADER_LEN - .saturating_add(version.as_bytes().len()) + .saturating_add(cbor_bytes_header_len(version)) + .saturating_add(version) .saturating_add(message.as_slice().len()) } @@ -153,10 +160,13 @@ impl LeafRun { pub fn push(&mut self, version: &Version, message: &Message) -> Result<(), LengthOverflow> { let version = version.as_bytes(); let message = message.as_slice(); - let len = version.len().saturating_add(message.len()); + let len = cbor_bytes_header_len(version.len()) + .saturating_add(version.len()) + .saturating_add(message.len()); let header = checked_record_header(len)?; self.bytes.reserve(LENGTH_HEADER_LEN + len); self.bytes.extend_from_slice(&header); + write_cbor_bytes_header(&mut self.bytes, version.len()); self.bytes.extend_from_slice(version); self.bytes.extend_from_slice(message); Ok(()) @@ -198,7 +208,7 @@ impl LeafRun { /// Iterate the run's records, decoding each into its canonical pair. pub fn records(&self) -> impl Iterator), DecodeLeafError>> where - T: BorshDeserialize, + T: DeserializeOwned, { self.record_slices().map(parse_record) } @@ -253,20 +263,69 @@ fn record_header(header: &[u8]) -> usize { } /// Decode one exact record body into its canonical pair. -fn parse_record( +fn parse_record( record: &[u8], ) -> Result<(Version, Message), DecodeLeafError> { - // The exact record body makes both Borsh values a single, non-retrying - // parse. + // Both fields are self-delimiting CBOR values, so the exact record + // body parses without retrying, and whatever the payload's parse does + // not consume is trailing. + fn de_error(e: ciborium::de::Error) -> std::io::Error { + match e { + ciborium::de::Error::Io(e) => e, + e => std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()), + } + } let mut input = record; - let version = Version::deserialize(&mut input).map_err(DecodeLeafError::Version)?; - let message = Message::deserialize(&mut input).map_err(DecodeLeafError::Message)?; + let version: Version = + ciborium::de::from_reader(&mut input).map_err(|e| DecodeLeafError::Version(de_error(e)))?; + let payload = input; + let message: T = + ciborium::de::from_reader(&mut input).map_err(|e| DecodeLeafError::Message(de_error(e)))?; if !input.is_empty() { return Err(DecodeLeafError::TrailingBytes { count: input.len() }); } + let message = Message::from_decoded(message, bytes::Bytes::copy_from_slice(payload)); Ok((version, message)) } +/// Bytes of the CBOR definite-length byte-string header for a `len`-byte +/// payload: the major-type-2 initial byte, plus the argument's width. +/// +/// The dual of [`write_cbor_bytes_header`]; `record_len` prices with one +/// and `push` writes with the other, and the +/// `record_len_matches_an_actual_push` pin holds them together. +fn cbor_bytes_header_len(len: usize) -> usize { + match len { + 0..=23 => 1, + 24..=0xff => 2, + 0x100..=0xffff => 3, + 0x1_0000..=0xffff_ffff => 5, + _ => 9, + } +} + +/// Append the CBOR definite-length byte-string header for a `len`-byte +/// payload: exactly what [`ciborium`] emits for `serialize_bytes`. +fn write_cbor_bytes_header(out: &mut Vec, len: usize) { + const MAJOR_BYTES: u8 = 2 << 5; + match len { + 0..=23 => out.push(MAJOR_BYTES | len as u8), + 24..=0xff => out.extend_from_slice(&[MAJOR_BYTES | 24, len as u8]), + 0x100..=0xffff => { + out.push(MAJOR_BYTES | 25); + out.extend_from_slice(&(len as u16).to_be_bytes()); + } + 0x1_0000..=0xffff_ffff => { + out.push(MAJOR_BYTES | 26); + out.extend_from_slice(&(len as u32).to_be_bytes()); + } + _ => { + out.push(MAJOR_BYTES | 27); + out.extend_from_slice(&(len as u64).to_be_bytes()); + } + } +} + /// A supply run whose record framing is structurally invalid. #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum LeafRunError { diff --git a/src/tree/mirror/streaming/remote/codec/frame/tests.rs b/src/tree/mirror/streaming/remote/codec/frame/tests.rs index db0b05976..3f2acdb0c 100644 --- a/src/tree/mirror/streaming/remote/codec/frame/tests.rs +++ b/src/tree/mirror/streaming/remote/codec/frame/tests.rs @@ -35,3 +35,50 @@ fn checked_header_encodes_the_bare_record_length() { (len as u32).to_be_bytes(), ); } + +/// `record_len` prices exactly what `push` writes, at every CBOR +/// byte-string header width a version can occupy. +/// +/// The two are the same quantity computed two ways — arithmetic against +/// actual encoding — so the run-budget math can trust the closed form. +/// Deep version chains grow the canonical encoding through the 1-byte +/// (< 24), 2-byte (< 256), and 3-byte (< 65536) CBOR header regimes; the +/// chain lengths below land encodings in the first two and the message +/// sizes sweep the payload term. +#[test] +fn record_len_matches_an_actual_push() { + let mut version = crate::Version::new(); + let mut checked_regimes = std::collections::BTreeSet::new(); + for parties in 1..=128u32 { + // One tick on a fresh disjoint party per step: each new party's + // event widens the canonical encoding, marching it through the + // CBOR header-width regimes. + version.tick(&crate::tree::arb::nth_party(parties as usize)); + if !(parties == 1 || parties % 17 == 0) { + continue; + } + checked_regimes.insert(super::cbor_bytes_header_len(version.as_bytes().len())); + for message in [Message::new(0u64), Message::new(u64::MAX)] { + let mut run = LeafRun::::new(); + run.push(&version, &message).expect("test records fit"); + assert_eq!( + run.encoded_len(), + LeafRun::record_len(&version, &message), + "record_len must price exactly one pushed record", + ); + // The version atom `push` writes is byte-identical to the + // serde form the decoder parses (ciborium's byte string). + let mut serde_form = Vec::new(); + ciborium::ser::into_writer(&version, &mut serde_form).unwrap(); + assert_eq!( + &run.as_bytes()[LENGTH_HEADER_LEN..LENGTH_HEADER_LEN + serde_form.len()], + serde_form.as_slice(), + "push's hand-written CBOR header must match ciborium's", + ); + } + } + assert!( + checked_regimes.len() >= 2, + "the sweep must cross at least two CBOR header-width regimes, got {checked_regimes:?}", + ); +} diff --git a/src/tree/mirror/streaming/remote/codec/snapshots/rumors__tree__mirror__streaming__remote__codec__tests__bounded_corpus_manifest_snapshot.snap b/src/tree/mirror/streaming/remote/codec/snapshots/rumors__tree__mirror__streaming__remote__codec__tests__bounded_corpus_manifest_snapshot.snap index d7d12038c..501e88d9b 100644 --- a/src/tree/mirror/streaming/remote/codec/snapshots/rumors__tree__mirror__streaming__remote__codec__tests__bounded_corpus_manifest_snapshot.snap +++ b/src/tree/mirror/streaming/remote/codec/snapshots/rumors__tree__mirror__streaming__remote__codec__tests__bounded_corpus_manifest_snapshot.snap @@ -10,8 +10,8 @@ Initiator QueryEmpty(End): cases 1 accepted 0 rejected 1 rejection Some(OpeningSupplies) digest bed1553e944c1f60caad77749acd1505d7b760208777c714f35ffdc0f2de766d Query(Continue): cases 32896 accepted 0 rejected 32896 rejection Some(OpeningSupplies) digest 9ba562d04ead543df788328565bd2f0dc9d4471e6e2bf7be09eec7d061dee9af Query(End): cases 32896 accepted 0 rejected 32896 rejection Some(OpeningSupplies) digest 387698c6a8f6cd20537f989b769a3033ffa03c71aaf4854dc14f2010d5673555 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 2feb92174c7a3441b754d0c3616c83ce2a641ab266b72707928c9a9f0051ef8a - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 329e4bf4805617b618025b822013d9a0f47162438e22e41039bb0e8930eb15b8 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 59b5fd7fcfce8862da5a311e322502cc8fdcd5b1ead61bd8e6b6e532bf774ede + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest e84ed624d4c85c71009ba4d06f28a9b3c251d88711e3ee37f9c5877b511222ad End(Reply): cases 1 accepted 1 rejected 0 rejection None digest e6c8657a09bbd68e3a4410183521d0dc564776c29489da7eef314c391632057b End(Stream): cases 1 accepted 1 rejected 0 rejection None digest c6137a2a9996157c93f59583278135d535fbd3d5439da4de7af076a1a699dc2d stream 01 @@ -21,8 +21,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 066871f0d0267b1b42edbb3835aed267fd755c6f7a819c1420def0671f62628d Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest af9a3ba555a3bcdfa2108b28f6539d8620aeb2eda089bfb7896820f98ba0a827 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest f29eaef5dc2c87a94692e1687365ee51a341ad822204604da619148f4f742247 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest c33e8e1bd317c7b329abc4170d74477862771bac68e98a3b960e3ea3fc910397 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 7ee40ff818bcf4527430cdee8f99cca9107eff4d0d9c3202bb2a03df96d8cc79 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 319b28c9c064036c6f2b1e30f665b32d6061b2f03a826d934a56845924030837 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 4dfd3a14bb679a3f3c72a3bac874d2ecb167299fcf03f1543fcdd49d76144db8 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 7efb04b3cd8ae0a43ffaaf027a7d1ae31f033da458ccf85ed6339227bdfce8c2 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest d210087cbc67d20f09c0fda1159b051ad38c629fe9783dfaf4e0cbbe985ecdcc stream 02 @@ -32,8 +32,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest b232233fad61fdfe489b32005295b43142c3da73df863366f532cf4e04dc0960 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 6c6a82a4201c2095453ffcc81b644af49180d33c49a958cbbd221bd259a931a5 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 1d2091676cd8510ae281d0959ba783251c31b696d764c878213a67cdddb2fe00 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 52d9343e92141851174d99737ed2e0bd1694a83214168bd34ea957d9d49d2aba - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest b9ae0d12ed4dfa9792480bf3ad185bb442f763ca9d3382a330881480e1006761 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 97b2682d961d4e0fbb9ca8093663428d6c5852e37baa1905f652fde30e31b0b3 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest fc1d59d32a9266c1a77486e6d76a9bee51d1bb4cd943d380038230f42b9dcf4b End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 5aaec826e8f1aa4bb1dc7f60095baf57a5b3cb9adcb0a4cf688407b0e7b7347e End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 2221d8b7741c597c23a781ae3a4315325660af324d10d667036b1c22c70fd4ff stream 03 @@ -43,8 +43,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 157b7b3c36e30debeb2364b3e9c29267d936ae5b68ac616aee5fc533a24b52fb Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest c51172601ee20ac3a04e5a32720d521af1af3a90d29b71d6337acbafbe5fc115 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest f427d3759ade3b024bfc7f57189200fbe0e220ecc832df29864cfccb0283a740 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest b38402ddf9b7e82f9cab82d6c72486ea1b1324768c4f779ec47776adc8cc2caa - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 1a890b189737db5e3d73dfd549c0acba90b897a91ddfb23bb520d45894988213 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest e3525955c5573149fbb36710cf3cca454db1eef8fc2d7dfbfbadd08096b6b14a + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 9f920581eb1636ed6e52b37f50109b03b6010e431e7173a0af29bfda972f1f64 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 8dc4e0a82ef1dfa3953132cd6cbe54650221b73a98162927793d488162a38fac End(Stream): cases 1 accepted 1 rejected 0 rejection None digest edc61faa00b781b3d047bab5809faf0b1a74bdb20f202712d9e5dbbf805525b8 stream 04 @@ -54,8 +54,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest bafaa10bfebbfd58e6dd9555c38d68f074a88cc7714d5cda3a8d4932990cd87f Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest aa3e0335828851588f14358567427c9634d7463286a2264198060a3d9c188e11 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest dfc58272001597c14dfa88433653257a8a3b12091c74fc18f40c6fe3c74a588d - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 2872f63e14448877bfccc0575c0d2d2043155c69f7bd268c065add4e4bca8063 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 25e8489a1bc0f94442adb0ccaeea22a7e10d9f6f2338937538201aa685d7382b + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 9d263dbb054de831ed5346a3301e63c7e338b02a834d0db1bcd11d1eb77105ad + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 4d2250bf01b9cc3ef793712d67669ccc61ef5c42984841d578bfd2a848b5331b End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 964e4c82824016b610d97562de5eb0d355b4d17cf16acb220581550c2cb08201 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 09f3d01b5a48fee382807fb80ccfae7e9a2aa167b28ed706e8cd6e536752e14f stream 05 @@ -65,8 +65,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 41fc3692f19ac323b3b2d7c8bd0307425676297f8e0bed6b221753c173461fd0 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 75abb3accb666e6145d70c2d102fdd6ce5f7ee38cfb7d4a33696051837bedbfd Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest fe90890210e48d3fac30d24e2fb66ad2a6273e6d8b67cff578a161b3062680c0 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest feb1e681d7204235a6f1bf914dad38a977c00b7e817d6974f5e50eb9f13877d9 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 989bda087527ecbad03e3fa3c9913c663ff4936f274be759466480942fe89026 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest b528e65d2440e583aead617a94d194f56699e05e71235a9229f907dced0a9b79 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest df3b29ef01094a59fa27126fbc554570a1b89e4be9cbd9f525279aefa812c774 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest f42bf364254585a59063ca719e3e466fabdcf8bb4de407f05c44bf8c1b75b0f4 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 37008b172b7470e27c3df9f915d753b7a1c501bdab556c600eddff42c51e5df1 stream 06 @@ -76,8 +76,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest e11362eed2d9395eae0b5e66d4adf8e7b26d5660c549658c61c500fc61921a97 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 14a04b8700e66157eb28db77fc19075aec4fb93ac3b4cf7ac8f4a7dc6d195548 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 9095d15c02ae8055927f39ada986d42a974f1621d7a97a48f22619dab7f51eb3 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest f886b7c964a752eadd25cfc3446ea658a3b41fd189765bfe8db9db3c0fbd15fc - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 7e9864b6d831cb1b7e3dc643119d1b81167d29a24b479e13ffa717b8ffe9c995 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest a2d7ee70ef3c9562a15d7077586fd0d24b4d8f81db5a4fb9869782458de72fb2 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest a4561e3b2a42f881801ad9ab5d79618254f5157ec1a50043c81d7a53f3245f72 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 29f62ebb0b44a04c6c29c315771602bf97d0e948fdfc9e889a3ec27572876e1f End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 127c84b8f7a667189b8accc3ead7f9d759231b6d5e9f4d521bf8a27acffd3e20 stream 07 @@ -87,8 +87,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 46acb5c7c4557b55a31329576477e76c7a9f9a41fb45b8c5907ed7b9ec11a433 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 66a2f0ee0a7799997880a907305e992f4ac33bf602d96761235310a2149c9bc8 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 5d04b5006b60a70a1413b657e481c3ed6d0a75da18ecc410b72d19872a2b17eb - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 14e23b62d8fdbe65c0659ca4d97591cc75974242a921ae29b1297c3ea0be940f - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest c5b06e84ec8cb75551fc0b79a04c764ffadd8013434a87c47fe6e3e215a93119 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 5e300b4cb071daa8574f20507a7da4472111351f1e740723f9b5bf8097b8e41b + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 0a562f5dd6f838c161f4f48188a8b8783acedd8f3938c965213ef59fb0e6d6e3 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 0abc151c61d1bf0abdbf8de84f9a373da4dd54f63d5ddde0d3963e68e7a1bf8e End(Stream): cases 1 accepted 1 rejected 0 rejection None digest ef47eedfe6e578be84ed170e1f5cfb0978893dda7ff8ea95c5e56c76947e5064 stream 08 @@ -98,8 +98,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 17f68790f5b708d77429611d19cade3367733a261a50c4b41037fd88026cbaa3 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 5851952e464d7e55c7f95efb554bc5613a5bdce4ccec542581674532a90ec418 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest e286e082de98f67d8a5e12fa2ce5d46de10d524b70764cd9de29d59be3b54c2c - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest baf2727e45f14040c4f66f4f7e3a9cd14ad59822412341343e6902d26ea2f32b - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest b102e8dc514abd70b113d16fdfe801f85a39b4fa4fb723f10fc70bfdcf4540b7 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest f0b52c1605a67276d58f1baeb1abb4b1ec603d3f5d037ce404b8052298b13828 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 130042645e4d5f9eac83eaa1271585f1147b8dcce58bffd94aecc4cac6e96b96 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 8bb8f6555c5bdf08019175230fe7e440d3ab688fa49909e75da4ac8c3e091809 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest f96f9bf58b82edb0d80376acd21f4f8b5458aeed6f63c82f79da08f83eb73601 stream 09 @@ -109,8 +109,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest f730acb629eef6f410a2464a59b555340285b5a9f4fa7c3c726353ff38c8e538 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 7bfb131a76928337a39d00599979ddacea1cc88fbea1f251eabc7ce2130a16d1 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 137950e7ae9ab307972b518682a1a6529819980301e0c6b61869477829624ce4 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest e0f6519782dcab95e0ae21d469221c5d04a4c1e074a55c539da1864a8b3eab8d - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest b45ab5b2be28d746edd23d13d22a8632c408a616ef304bdfc5a64fdcc02d9c23 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 138fd3f03e645cf6dbce8b55d67dfa3bee2d319611ccbd87fc5b323747ea818b + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest b19854ff51cedbf8a4e761891538cdd8a2466a8111f5a321f530c1c23c685b5b End(Reply): cases 1 accepted 1 rejected 0 rejection None digest b1b6b3a0adeaf7bdf761e9a23a79f238199e020dd452c21e9f5aab6a1da5d82b End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 27cf3d734488f83f038cb377078fe6cc24925393b66c6c31fe2960a6107a7415 stream 10 @@ -120,8 +120,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest e74e1793309ec1319e8638cf40ea30ecda33dac3ac34e039080e2c6b0fd9f4cc Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 1a0630fb34fbcf3cd72eb700271d55cb72d1396cf043d7594b4d011e06de3d65 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 16d0104cd8cc53d64ee80959c406a0b544dcf97efc2a8d4883b8372bdb8107dc - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest cdd3394d1695b6412d651e00563dc12e41edd2f96c709e566a392bf5ad597595 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 6ebd048e1020ba943038c5118be9afdf978d71be00a2d1ac6946f0f9e1c4e7e0 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 09a2b373f6a05b7868f0b62badd9a7b70485c3887d07f3704e17287a495bdd0e + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest cf53b4f42b6df5f38a07ecb6dbae883816b89d0776d27fd781f8fa08105c0409 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 63c674a610aa789bcab890eb7b995a513ea52f2cb4d35a3e773836850070c932 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 299a727b9d68956daf0449e6984cb16c4b2164d84eb1458c06226902204b6609 stream 11 @@ -131,8 +131,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 851a1229d333e606ec2aeb7fffe0f817662f5f010010af9b62ae437d2e60f4d2 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest c930467f2aaeee953347042808ebb417d92cd89f56335f085d0e468f33d61ffa Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 20b37b9884de14193cca46bda29983412944b68c277fd6930dd5f60df04f782d - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest fd418fb2953df411842dd0ec52c64982bd7893a50a2c5b17b50061a821c25ea2 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 0ae7f4b78b28051b2dfc0233819cb25449a52c1f2563ab05e2000b0fefb4174a + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 5611b1ec26af613d14ebb77b8a17f0efc88b9c5da0c29776e9692aa12ee2af76 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest e504ec6c7b648cd9e1a489be7954ed0208ff6cfb264e366ba0a300da9c1820f7 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest d8157eee870ca226593b505a153b951f657311ca5471860ee29e67aa548dd58b End(Stream): cases 1 accepted 1 rejected 0 rejection None digest a1351b92d9e69aaaee98b9ecf17a33f6e3a9e0270b149290f2d45cba7c6d6149 stream 12 @@ -142,8 +142,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest a31da823b3c1bb429765cc210a258f220aaa8c92df9673eb382fe12821b111ac Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 254975c65f08456de3e7be51da5139736a55a1b5e7e9a6defbaa7a6dd95a0523 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest c54fb8b09ef9fea57ae284009d05a41b8f993e06114b4ff918d8565b2bb43d3f - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 4a0a86c4f3c19a97539c08219d8dfe40bc7db3f42ada8119a591f63fc2860b50 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 6a1bbe6eaea48f3f96e0adc56b4c010ee6afe660a50d49356dacb507a2acd788 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 0d05f27f04bd9868452c564d84baea9c6ce14927eb4ce469ab9a6246fa26d926 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 638ae22a8f314323aa7353af461430680e1fafa580f11ac1f73f21fc1e8d9d95 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 5a3f1429c3d154082905a1e51ffd43a59cc80a48fb38e85abd61598fdca8dedc End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 0e99767e69250ac6fcbfb7b4d9c993c83ecf9077dc80bd989d10d8e16687c1a7 stream 13 @@ -153,8 +153,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 4fd9741405c8cc1354022519f755d08d511ea2455235de82b292d0bac0f2da9d Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 3c5d1011b48719d8d6b69ecbf983be0b3fb6757f0c4eea62740babdf03888f0a Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest e5abecd847278d559eb1e90add494e476aafb8b8a7b6da3adc46314c003c3d91 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 91099eef8ecf0275e5d6a88d9bd57644839c56b5024c64cc3e88e0c62efc1ee4 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest f960157e41bdc2d7cb0a1129e29a0e1e2817ac45d8338ceb1eb5e6a35a01be0f + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest bed90570bb0963ab5526ce6e55abda3be957893a5177c318819af3a7f46e29b0 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 7d73dd645bbf5331ce6e68dcdde05bf0dd9057592b8d5d02c651e8855abbdd6b End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 785f77fdc47845b843501b702d0260af3ba977a343a8763a874321e7769160f7 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest b1b179df568a7c34c8d7bf87203c161ef88e1ab38ca0d75e6557fd3527dc11a7 stream 14 @@ -164,8 +164,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 5d537842e1e44c2fb2663485c69818515a5d724e84ab275eeeda1534a66b326f Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest b0adb97cf6e8fd75a3d6eca5d1ba6a81b4e9cfbce99793d9374bf35e6a53f756 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 1a598804994f12a0bf7db52814ab606b664bb716c6077797d25ca28777067d00 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 2c53ce0d92784907c856d4fd4ee664556c1b276b99921195eb942c6a90d8c66f - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest c1c02a2a851344cb43963ef857e8f7ebed725e6ed698aa9adfb0bc8378fa9c86 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest b8637f87ab12074b33dd6ef0cf82da1a1e576167190c225c72f9578963208c00 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 28a6bede5bd63c5a42a5f30e4f7dbcc3628634af2962a8e8839ae4b9c29f604d End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 2f182802ac4c754db7b1e709ec1e2c62fed5a070e2c068faba47bb7964d3f4e2 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest f5c41bf5e61ad5831f4929f863cfa6bfe71fa3b6ee9b27a21e7c0a7595e357b7 stream 15 @@ -175,8 +175,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 627c3e7aff204f684040fcf3fd123b5c9cfb945c9aae4167b12eab4723b953f0 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest b8bef3ff0962d6a257250d4bb4876d9f1d9aab401cd818b3e9a2c9475e1ec67e Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 97fec0191ef2a75b3d6440739172679e85e8c853f502199048fa9c34fe479ad9 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest d93dbacbbd65c03d0ac661e7aa70885e6065c56321361cda629965a13a655fc7 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 26fb5d0f78959853eb5b4083a718bd156053396a16768a9b01e9e0d761a8a332 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest e2a690dbe15e9f6276788dee265fa948b186a5904698d55aa72c1ebf49bd7b9f + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest b10ff7406dbceaf57f9edfd790369eda2149677347a3bc5a15226dbe0928d0ca End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 033a9d6c6cb059c17270280ac6121bc6cff59ff0b452c49cb65dce6f55e992a7 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest d4fc5053bcf8daa493d15dfb6750582b5efc3690d8eb3260ed4645e012789c4b stream 16 @@ -186,8 +186,8 @@ Initiator QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest ba0d629849bd7c219c3ecb424e75a17049097500732e7887fd7b951eaaac4fbe Query(Continue): cases 32896 accepted 0 rejected 32896 rejection Some(LeafParentReplies) digest d678b609a984aec0e82f6704ec78d2c1312a319c00aef09315cb6d5a97afd3b3 Query(End): cases 32896 accepted 0 rejected 32896 rejection Some(LeafParentReplies) digest 20206e46e9569954fa7409adc1466295dd78517ee9b1f7a6dcb931af522b09f2 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 47493d906b49a371d6db3a5d99ad7a8f16390fe55e715b084a949a593157a775 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 059bd5afec8c19e0ffdffdd06969c8f56e3591f047cc7c6af8dac17fe687ca23 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 9f508aa9bb69f2b3742ce4cd27f52fa10dc7526ce8a0f529d4ab3e922ca0c6c2 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 29c688db20ab6a6a85b8a64ea17a0f0c9e3380c16c02a687588891c443cd6664 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 7a250d61fe85b397ff21c6c0d2883c934615f0c59d3f7fa92090f2bbc974cb74 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 69e0a19208d6edd30e31916235b05fcd9f3072a9f93668a49e7cd2d971621588 Responder @@ -198,8 +198,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest b90c5effa96b16776cbf6e9949635b52682c2c65c7f797ff18573310cd77339c Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 7c974c0efd6be446db31c057cb8a5c8ab3be81b87a9c0dbeb9c9465088e2a743 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest c1aa9df70be5616b950459c84c1da97879b7228dd5221b651b098cc4728d9ad8 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 2feb92174c7a3441b754d0c3616c83ce2a641ab266b72707928c9a9f0051ef8a - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 329e4bf4805617b618025b822013d9a0f47162438e22e41039bb0e8930eb15b8 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 59b5fd7fcfce8862da5a311e322502cc8fdcd5b1ead61bd8e6b6e532bf774ede + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest e84ed624d4c85c71009ba4d06f28a9b3c251d88711e3ee37f9c5877b511222ad End(Reply): cases 1 accepted 1 rejected 0 rejection None digest e6c8657a09bbd68e3a4410183521d0dc564776c29489da7eef314c391632057b End(Stream): cases 1 accepted 1 rejected 0 rejection None digest c6137a2a9996157c93f59583278135d535fbd3d5439da4de7af076a1a699dc2d stream 01 @@ -209,8 +209,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 066871f0d0267b1b42edbb3835aed267fd755c6f7a819c1420def0671f62628d Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest af9a3ba555a3bcdfa2108b28f6539d8620aeb2eda089bfb7896820f98ba0a827 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest f29eaef5dc2c87a94692e1687365ee51a341ad822204604da619148f4f742247 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest c33e8e1bd317c7b329abc4170d74477862771bac68e98a3b960e3ea3fc910397 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 7ee40ff818bcf4527430cdee8f99cca9107eff4d0d9c3202bb2a03df96d8cc79 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 319b28c9c064036c6f2b1e30f665b32d6061b2f03a826d934a56845924030837 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 4dfd3a14bb679a3f3c72a3bac874d2ecb167299fcf03f1543fcdd49d76144db8 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 7efb04b3cd8ae0a43ffaaf027a7d1ae31f033da458ccf85ed6339227bdfce8c2 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest d210087cbc67d20f09c0fda1159b051ad38c629fe9783dfaf4e0cbbe985ecdcc stream 02 @@ -220,8 +220,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest b232233fad61fdfe489b32005295b43142c3da73df863366f532cf4e04dc0960 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 6c6a82a4201c2095453ffcc81b644af49180d33c49a958cbbd221bd259a931a5 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 1d2091676cd8510ae281d0959ba783251c31b696d764c878213a67cdddb2fe00 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 52d9343e92141851174d99737ed2e0bd1694a83214168bd34ea957d9d49d2aba - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest b9ae0d12ed4dfa9792480bf3ad185bb442f763ca9d3382a330881480e1006761 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 97b2682d961d4e0fbb9ca8093663428d6c5852e37baa1905f652fde30e31b0b3 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest fc1d59d32a9266c1a77486e6d76a9bee51d1bb4cd943d380038230f42b9dcf4b End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 5aaec826e8f1aa4bb1dc7f60095baf57a5b3cb9adcb0a4cf688407b0e7b7347e End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 2221d8b7741c597c23a781ae3a4315325660af324d10d667036b1c22c70fd4ff stream 03 @@ -231,8 +231,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 157b7b3c36e30debeb2364b3e9c29267d936ae5b68ac616aee5fc533a24b52fb Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest c51172601ee20ac3a04e5a32720d521af1af3a90d29b71d6337acbafbe5fc115 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest f427d3759ade3b024bfc7f57189200fbe0e220ecc832df29864cfccb0283a740 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest b38402ddf9b7e82f9cab82d6c72486ea1b1324768c4f779ec47776adc8cc2caa - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 1a890b189737db5e3d73dfd549c0acba90b897a91ddfb23bb520d45894988213 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest e3525955c5573149fbb36710cf3cca454db1eef8fc2d7dfbfbadd08096b6b14a + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 9f920581eb1636ed6e52b37f50109b03b6010e431e7173a0af29bfda972f1f64 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 8dc4e0a82ef1dfa3953132cd6cbe54650221b73a98162927793d488162a38fac End(Stream): cases 1 accepted 1 rejected 0 rejection None digest edc61faa00b781b3d047bab5809faf0b1a74bdb20f202712d9e5dbbf805525b8 stream 04 @@ -242,8 +242,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest bafaa10bfebbfd58e6dd9555c38d68f074a88cc7714d5cda3a8d4932990cd87f Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest aa3e0335828851588f14358567427c9634d7463286a2264198060a3d9c188e11 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest dfc58272001597c14dfa88433653257a8a3b12091c74fc18f40c6fe3c74a588d - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 2872f63e14448877bfccc0575c0d2d2043155c69f7bd268c065add4e4bca8063 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 25e8489a1bc0f94442adb0ccaeea22a7e10d9f6f2338937538201aa685d7382b + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 9d263dbb054de831ed5346a3301e63c7e338b02a834d0db1bcd11d1eb77105ad + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 4d2250bf01b9cc3ef793712d67669ccc61ef5c42984841d578bfd2a848b5331b End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 964e4c82824016b610d97562de5eb0d355b4d17cf16acb220581550c2cb08201 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 09f3d01b5a48fee382807fb80ccfae7e9a2aa167b28ed706e8cd6e536752e14f stream 05 @@ -253,8 +253,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 41fc3692f19ac323b3b2d7c8bd0307425676297f8e0bed6b221753c173461fd0 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 75abb3accb666e6145d70c2d102fdd6ce5f7ee38cfb7d4a33696051837bedbfd Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest fe90890210e48d3fac30d24e2fb66ad2a6273e6d8b67cff578a161b3062680c0 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest feb1e681d7204235a6f1bf914dad38a977c00b7e817d6974f5e50eb9f13877d9 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 989bda087527ecbad03e3fa3c9913c663ff4936f274be759466480942fe89026 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest b528e65d2440e583aead617a94d194f56699e05e71235a9229f907dced0a9b79 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest df3b29ef01094a59fa27126fbc554570a1b89e4be9cbd9f525279aefa812c774 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest f42bf364254585a59063ca719e3e466fabdcf8bb4de407f05c44bf8c1b75b0f4 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 37008b172b7470e27c3df9f915d753b7a1c501bdab556c600eddff42c51e5df1 stream 06 @@ -264,8 +264,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest e11362eed2d9395eae0b5e66d4adf8e7b26d5660c549658c61c500fc61921a97 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 14a04b8700e66157eb28db77fc19075aec4fb93ac3b4cf7ac8f4a7dc6d195548 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 9095d15c02ae8055927f39ada986d42a974f1621d7a97a48f22619dab7f51eb3 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest f886b7c964a752eadd25cfc3446ea658a3b41fd189765bfe8db9db3c0fbd15fc - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 7e9864b6d831cb1b7e3dc643119d1b81167d29a24b479e13ffa717b8ffe9c995 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest a2d7ee70ef3c9562a15d7077586fd0d24b4d8f81db5a4fb9869782458de72fb2 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest a4561e3b2a42f881801ad9ab5d79618254f5157ec1a50043c81d7a53f3245f72 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 29f62ebb0b44a04c6c29c315771602bf97d0e948fdfc9e889a3ec27572876e1f End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 127c84b8f7a667189b8accc3ead7f9d759231b6d5e9f4d521bf8a27acffd3e20 stream 07 @@ -275,8 +275,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 46acb5c7c4557b55a31329576477e76c7a9f9a41fb45b8c5907ed7b9ec11a433 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 66a2f0ee0a7799997880a907305e992f4ac33bf602d96761235310a2149c9bc8 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 5d04b5006b60a70a1413b657e481c3ed6d0a75da18ecc410b72d19872a2b17eb - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 14e23b62d8fdbe65c0659ca4d97591cc75974242a921ae29b1297c3ea0be940f - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest c5b06e84ec8cb75551fc0b79a04c764ffadd8013434a87c47fe6e3e215a93119 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 5e300b4cb071daa8574f20507a7da4472111351f1e740723f9b5bf8097b8e41b + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 0a562f5dd6f838c161f4f48188a8b8783acedd8f3938c965213ef59fb0e6d6e3 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 0abc151c61d1bf0abdbf8de84f9a373da4dd54f63d5ddde0d3963e68e7a1bf8e End(Stream): cases 1 accepted 1 rejected 0 rejection None digest ef47eedfe6e578be84ed170e1f5cfb0978893dda7ff8ea95c5e56c76947e5064 stream 08 @@ -286,8 +286,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 17f68790f5b708d77429611d19cade3367733a261a50c4b41037fd88026cbaa3 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 5851952e464d7e55c7f95efb554bc5613a5bdce4ccec542581674532a90ec418 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest e286e082de98f67d8a5e12fa2ce5d46de10d524b70764cd9de29d59be3b54c2c - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest baf2727e45f14040c4f66f4f7e3a9cd14ad59822412341343e6902d26ea2f32b - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest b102e8dc514abd70b113d16fdfe801f85a39b4fa4fb723f10fc70bfdcf4540b7 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest f0b52c1605a67276d58f1baeb1abb4b1ec603d3f5d037ce404b8052298b13828 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 130042645e4d5f9eac83eaa1271585f1147b8dcce58bffd94aecc4cac6e96b96 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 8bb8f6555c5bdf08019175230fe7e440d3ab688fa49909e75da4ac8c3e091809 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest f96f9bf58b82edb0d80376acd21f4f8b5458aeed6f63c82f79da08f83eb73601 stream 09 @@ -297,8 +297,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest f730acb629eef6f410a2464a59b555340285b5a9f4fa7c3c726353ff38c8e538 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 7bfb131a76928337a39d00599979ddacea1cc88fbea1f251eabc7ce2130a16d1 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 137950e7ae9ab307972b518682a1a6529819980301e0c6b61869477829624ce4 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest e0f6519782dcab95e0ae21d469221c5d04a4c1e074a55c539da1864a8b3eab8d - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest b45ab5b2be28d746edd23d13d22a8632c408a616ef304bdfc5a64fdcc02d9c23 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 138fd3f03e645cf6dbce8b55d67dfa3bee2d319611ccbd87fc5b323747ea818b + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest b19854ff51cedbf8a4e761891538cdd8a2466a8111f5a321f530c1c23c685b5b End(Reply): cases 1 accepted 1 rejected 0 rejection None digest b1b6b3a0adeaf7bdf761e9a23a79f238199e020dd452c21e9f5aab6a1da5d82b End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 27cf3d734488f83f038cb377078fe6cc24925393b66c6c31fe2960a6107a7415 stream 10 @@ -308,8 +308,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest e74e1793309ec1319e8638cf40ea30ecda33dac3ac34e039080e2c6b0fd9f4cc Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 1a0630fb34fbcf3cd72eb700271d55cb72d1396cf043d7594b4d011e06de3d65 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 16d0104cd8cc53d64ee80959c406a0b544dcf97efc2a8d4883b8372bdb8107dc - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest cdd3394d1695b6412d651e00563dc12e41edd2f96c709e566a392bf5ad597595 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 6ebd048e1020ba943038c5118be9afdf978d71be00a2d1ac6946f0f9e1c4e7e0 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 09a2b373f6a05b7868f0b62badd9a7b70485c3887d07f3704e17287a495bdd0e + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest cf53b4f42b6df5f38a07ecb6dbae883816b89d0776d27fd781f8fa08105c0409 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 63c674a610aa789bcab890eb7b995a513ea52f2cb4d35a3e773836850070c932 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 299a727b9d68956daf0449e6984cb16c4b2164d84eb1458c06226902204b6609 stream 11 @@ -319,8 +319,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 851a1229d333e606ec2aeb7fffe0f817662f5f010010af9b62ae437d2e60f4d2 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest c930467f2aaeee953347042808ebb417d92cd89f56335f085d0e468f33d61ffa Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 20b37b9884de14193cca46bda29983412944b68c277fd6930dd5f60df04f782d - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest fd418fb2953df411842dd0ec52c64982bd7893a50a2c5b17b50061a821c25ea2 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 0ae7f4b78b28051b2dfc0233819cb25449a52c1f2563ab05e2000b0fefb4174a + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 5611b1ec26af613d14ebb77b8a17f0efc88b9c5da0c29776e9692aa12ee2af76 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest e504ec6c7b648cd9e1a489be7954ed0208ff6cfb264e366ba0a300da9c1820f7 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest d8157eee870ca226593b505a153b951f657311ca5471860ee29e67aa548dd58b End(Stream): cases 1 accepted 1 rejected 0 rejection None digest a1351b92d9e69aaaee98b9ecf17a33f6e3a9e0270b149290f2d45cba7c6d6149 stream 12 @@ -330,8 +330,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest a31da823b3c1bb429765cc210a258f220aaa8c92df9673eb382fe12821b111ac Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 254975c65f08456de3e7be51da5139736a55a1b5e7e9a6defbaa7a6dd95a0523 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest c54fb8b09ef9fea57ae284009d05a41b8f993e06114b4ff918d8565b2bb43d3f - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 4a0a86c4f3c19a97539c08219d8dfe40bc7db3f42ada8119a591f63fc2860b50 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 6a1bbe6eaea48f3f96e0adc56b4c010ee6afe660a50d49356dacb507a2acd788 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 0d05f27f04bd9868452c564d84baea9c6ce14927eb4ce469ab9a6246fa26d926 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 638ae22a8f314323aa7353af461430680e1fafa580f11ac1f73f21fc1e8d9d95 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 5a3f1429c3d154082905a1e51ffd43a59cc80a48fb38e85abd61598fdca8dedc End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 0e99767e69250ac6fcbfb7b4d9c993c83ecf9077dc80bd989d10d8e16687c1a7 stream 13 @@ -341,8 +341,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 4fd9741405c8cc1354022519f755d08d511ea2455235de82b292d0bac0f2da9d Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest 3c5d1011b48719d8d6b69ecbf983be0b3fb6757f0c4eea62740babdf03888f0a Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest e5abecd847278d559eb1e90add494e476aafb8b8a7b6da3adc46314c003c3d91 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 91099eef8ecf0275e5d6a88d9bd57644839c56b5024c64cc3e88e0c62efc1ee4 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest f960157e41bdc2d7cb0a1129e29a0e1e2817ac45d8338ceb1eb5e6a35a01be0f + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest bed90570bb0963ab5526ce6e55abda3be957893a5177c318819af3a7f46e29b0 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 7d73dd645bbf5331ce6e68dcdde05bf0dd9057592b8d5d02c651e8855abbdd6b End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 785f77fdc47845b843501b702d0260af3ba977a343a8763a874321e7769160f7 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest b1b179df568a7c34c8d7bf87203c161ef88e1ab38ca0d75e6557fd3527dc11a7 stream 14 @@ -352,8 +352,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 5d537842e1e44c2fb2663485c69818515a5d724e84ab275eeeda1534a66b326f Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest b0adb97cf6e8fd75a3d6eca5d1ba6a81b4e9cfbce99793d9374bf35e6a53f756 Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 1a598804994f12a0bf7db52814ab606b664bb716c6077797d25ca28777067d00 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest 2c53ce0d92784907c856d4fd4ee664556c1b276b99921195eb942c6a90d8c66f - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest c1c02a2a851344cb43963ef857e8f7ebed725e6ed698aa9adfb0bc8378fa9c86 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest b8637f87ab12074b33dd6ef0cf82da1a1e576167190c225c72f9578963208c00 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 28a6bede5bd63c5a42a5f30e4f7dbcc3628634af2962a8e8839ae4b9c29f604d End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 2f182802ac4c754db7b1e709ec1e2c62fed5a070e2c068faba47bb7964d3f4e2 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest f5c41bf5e61ad5831f4929f863cfa6bfe71fa3b6ee9b27a21e7c0a7595e357b7 stream 15 @@ -363,8 +363,8 @@ Responder QueryEmpty(End): cases 1 accepted 1 rejected 0 rejection None digest 627c3e7aff204f684040fcf3fd123b5c9cfb945c9aae4167b12eab4723b953f0 Query(Continue): cases 32896 accepted 32896 rejected 0 rejection None digest b8bef3ff0962d6a257250d4bb4876d9f1d9aab401cd818b3e9a2c9475e1ec67e Query(End): cases 32896 accepted 32896 rejected 0 rejection None digest 97fec0191ef2a75b3d6440739172679e85e8c853f502199048fa9c34fe479ad9 - Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest d93dbacbbd65c03d0ac661e7aa70885e6065c56321361cda629965a13a655fc7 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 26fb5d0f78959853eb5b4083a718bd156053396a16768a9b01e9e0d761a8a332 + Supply(Continue): cases 1 accepted 1 rejected 0 rejection None digest e2a690dbe15e9f6276788dee265fa948b186a5904698d55aa72c1ebf49bd7b9f + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest b10ff7406dbceaf57f9edfd790369eda2149677347a3bc5a15226dbe0928d0ca End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 033a9d6c6cb059c17270280ac6121bc6cff59ff0b452c49cb65dce6f55e992a7 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest d4fc5053bcf8daa493d15dfb6750582b5efc3690d8eb3260ed4645e012789c4b stream 16 @@ -375,6 +375,6 @@ Responder Query(Continue): cases 32896 accepted 0 rejected 32896 rejection Some(TerminalLeafReplies) digest d678b609a984aec0e82f6704ec78d2c1312a319c00aef09315cb6d5a97afd3b3 Query(End): cases 32896 accepted 0 rejected 32896 rejection Some(TerminalLeafReplies) digest 20206e46e9569954fa7409adc1466295dd78517ee9b1f7a6dcb931af522b09f2 Supply(Continue): cases 1 accepted 0 rejected 1 rejection Some(TerminalLeafReplies) digest 8a98948334763852e22cefc20322ea74bfef9f5b97b0c1ee2b11ccf8281288b3 - Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 059bd5afec8c19e0ffdffdd06969c8f56e3591f047cc7c6af8dac17fe687ca23 + Supply(End): cases 1 accepted 1 rejected 0 rejection None digest 29c688db20ab6a6a85b8a64ea17a0f0c9e3380c16c02a687588891c443cd6664 End(Reply): cases 1 accepted 1 rejected 0 rejection None digest 7a250d61fe85b397ff21c6c0d2883c934615f0c59d3f7fa92090f2bbc974cb74 End(Stream): cases 1 accepted 1 rejected 0 rejection None digest 69e0a19208d6edd30e31916235b05fcd9f3072a9f93668a49e7cd2d971621588 diff --git a/src/tree/mirror/streaming/remote/codec/snapshots/rumors__tree__mirror__streaming__remote__codec__tests__canonical_frame_atlas_snapshot.snap b/src/tree/mirror/streaming/remote/codec/snapshots/rumors__tree__mirror__streaming__remote__codec__tests__canonical_frame_atlas_snapshot.snap index 5f7e2e4dc..620c353a9 100644 --- a/src/tree/mirror/streaming/remote/codec/snapshots/rumors__tree__mirror__streaming__remote__codec__tests__canonical_frame_atlas_snapshot.snap +++ b/src/tree/mirror/streaming/remote/codec/snapshots/rumors__tree__mirror__streaming__remote__codec__tests__canonical_frame_atlas_snapshot.snap @@ -10,8 +10,8 @@ Initiator QueryEmpty(End): rejected byte 33 class OpeningSupplies Query(Continue): rejected byte 44 class OpeningSupplies Query(End): rejected byte 55 class OpeningSupplies - Supply(Continue): accepted len 10 hex 660000000500000001e0 - Supply(End): accepted len 10 hex 770000000500000001e0 + Supply(Continue): accepted len 12 hex 66000000070000000341e0f6 + Supply(End): accepted len 12 hex 77000000070000000341e0f6 End(Reply): accepted len 1 hex 88 End(Stream): accepted len 1 hex 99 stream 01 @@ -21,8 +21,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 34 Query(Continue): accepted len 27 hex 450000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 560000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 670000000500000001e0 - Supply(End): accepted len 10 hex 780000000500000001e0 + Supply(Continue): accepted len 12 hex 67000000070000000341e0f6 + Supply(End): accepted len 12 hex 78000000070000000341e0f6 End(Reply): accepted len 1 hex 89 End(Stream): accepted len 1 hex 9a stream 02 @@ -32,8 +32,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 35 Query(Continue): accepted len 27 hex 460000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 570000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 680000000500000001e0 - Supply(End): accepted len 10 hex 790000000500000001e0 + Supply(Continue): accepted len 12 hex 68000000070000000341e0f6 + Supply(End): accepted len 12 hex 79000000070000000341e0f6 End(Reply): accepted len 1 hex 8a End(Stream): accepted len 1 hex 9b stream 03 @@ -43,8 +43,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 36 Query(Continue): accepted len 27 hex 470000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 580000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 690000000500000001e0 - Supply(End): accepted len 10 hex 7a0000000500000001e0 + Supply(Continue): accepted len 12 hex 69000000070000000341e0f6 + Supply(End): accepted len 12 hex 7a000000070000000341e0f6 End(Reply): accepted len 1 hex 8b End(Stream): accepted len 1 hex 9c stream 04 @@ -54,8 +54,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 37 Query(Continue): accepted len 27 hex 480000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 590000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 6a0000000500000001e0 - Supply(End): accepted len 10 hex 7b0000000500000001e0 + Supply(Continue): accepted len 12 hex 6a000000070000000341e0f6 + Supply(End): accepted len 12 hex 7b000000070000000341e0f6 End(Reply): accepted len 1 hex 8c End(Stream): accepted len 1 hex 9d stream 05 @@ -65,8 +65,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 38 Query(Continue): accepted len 27 hex 490000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 5a0000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 6b0000000500000001e0 - Supply(End): accepted len 10 hex 7c0000000500000001e0 + Supply(Continue): accepted len 12 hex 6b000000070000000341e0f6 + Supply(End): accepted len 12 hex 7c000000070000000341e0f6 End(Reply): accepted len 1 hex 8d End(Stream): accepted len 1 hex 9e stream 06 @@ -76,8 +76,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 39 Query(Continue): accepted len 27 hex 4a0000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 5b0000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 6c0000000500000001e0 - Supply(End): accepted len 10 hex 7d0000000500000001e0 + Supply(Continue): accepted len 12 hex 6c000000070000000341e0f6 + Supply(End): accepted len 12 hex 7d000000070000000341e0f6 End(Reply): accepted len 1 hex 8e End(Stream): accepted len 1 hex 9f stream 07 @@ -87,8 +87,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 3a Query(Continue): accepted len 27 hex 4b0000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 5c0000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 6d0000000500000001e0 - Supply(End): accepted len 10 hex 7e0000000500000001e0 + Supply(Continue): accepted len 12 hex 6d000000070000000341e0f6 + Supply(End): accepted len 12 hex 7e000000070000000341e0f6 End(Reply): accepted len 1 hex 8f End(Stream): accepted len 1 hex a0 stream 08 @@ -98,8 +98,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 3b Query(Continue): accepted len 27 hex 4c0000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 5d0000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 6e0000000500000001e0 - Supply(End): accepted len 10 hex 7f0000000500000001e0 + Supply(Continue): accepted len 12 hex 6e000000070000000341e0f6 + Supply(End): accepted len 12 hex 7f000000070000000341e0f6 End(Reply): accepted len 1 hex 90 End(Stream): accepted len 1 hex a1 stream 09 @@ -109,8 +109,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 3c Query(Continue): accepted len 27 hex 4d0000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 5e0000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 6f0000000500000001e0 - Supply(End): accepted len 10 hex 800000000500000001e0 + Supply(Continue): accepted len 12 hex 6f000000070000000341e0f6 + Supply(End): accepted len 12 hex 80000000070000000341e0f6 End(Reply): accepted len 1 hex 91 End(Stream): accepted len 1 hex a2 stream 10 @@ -120,8 +120,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 3d Query(Continue): accepted len 27 hex 4e0000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 5f0000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 700000000500000001e0 - Supply(End): accepted len 10 hex 810000000500000001e0 + Supply(Continue): accepted len 12 hex 70000000070000000341e0f6 + Supply(End): accepted len 12 hex 81000000070000000341e0f6 End(Reply): accepted len 1 hex 92 End(Stream): accepted len 1 hex a3 stream 11 @@ -131,8 +131,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 3e Query(Continue): accepted len 27 hex 4f0000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 600000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 710000000500000001e0 - Supply(End): accepted len 10 hex 820000000500000001e0 + Supply(Continue): accepted len 12 hex 71000000070000000341e0f6 + Supply(End): accepted len 12 hex 82000000070000000341e0f6 End(Reply): accepted len 1 hex 93 End(Stream): accepted len 1 hex a4 stream 12 @@ -142,8 +142,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 3f Query(Continue): accepted len 27 hex 500000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 610000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 720000000500000001e0 - Supply(End): accepted len 10 hex 830000000500000001e0 + Supply(Continue): accepted len 12 hex 72000000070000000341e0f6 + Supply(End): accepted len 12 hex 83000000070000000341e0f6 End(Reply): accepted len 1 hex 94 End(Stream): accepted len 1 hex a5 stream 13 @@ -153,8 +153,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 40 Query(Continue): accepted len 27 hex 510000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 620000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 730000000500000001e0 - Supply(End): accepted len 10 hex 840000000500000001e0 + Supply(Continue): accepted len 12 hex 73000000070000000341e0f6 + Supply(End): accepted len 12 hex 84000000070000000341e0f6 End(Reply): accepted len 1 hex 95 End(Stream): accepted len 1 hex a6 stream 14 @@ -164,8 +164,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 41 Query(Continue): accepted len 27 hex 520000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 630000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 740000000500000001e0 - Supply(End): accepted len 10 hex 850000000500000001e0 + Supply(Continue): accepted len 12 hex 74000000070000000341e0f6 + Supply(End): accepted len 12 hex 85000000070000000341e0f6 End(Reply): accepted len 1 hex 96 End(Stream): accepted len 1 hex a7 stream 15 @@ -175,8 +175,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 42 Query(Continue): accepted len 27 hex 530000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 640000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 750000000500000001e0 - Supply(End): accepted len 10 hex 860000000500000001e0 + Supply(Continue): accepted len 12 hex 75000000070000000341e0f6 + Supply(End): accepted len 12 hex 86000000070000000341e0f6 End(Reply): accepted len 1 hex 97 End(Stream): accepted len 1 hex a8 stream 16 @@ -186,8 +186,8 @@ Initiator QueryEmpty(End): accepted len 1 hex 43 Query(Continue): rejected byte 54 class LeafParentReplies Query(End): rejected byte 65 class LeafParentReplies - Supply(Continue): accepted len 10 hex 760000000500000001e0 - Supply(End): accepted len 10 hex 870000000500000001e0 + Supply(Continue): accepted len 12 hex 76000000070000000341e0f6 + Supply(End): accepted len 12 hex 87000000070000000341e0f6 End(Reply): accepted len 1 hex 98 End(Stream): accepted len 1 hex a9 Responder @@ -198,8 +198,8 @@ Responder QueryEmpty(End): accepted len 1 hex 33 Query(Continue): accepted len 27 hex 440000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 550000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 660000000500000001e0 - Supply(End): accepted len 10 hex 770000000500000001e0 + Supply(Continue): accepted len 12 hex 66000000070000000341e0f6 + Supply(End): accepted len 12 hex 77000000070000000341e0f6 End(Reply): accepted len 1 hex 88 End(Stream): accepted len 1 hex 99 stream 01 @@ -209,8 +209,8 @@ Responder QueryEmpty(End): accepted len 1 hex 34 Query(Continue): accepted len 27 hex 450000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 560000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 670000000500000001e0 - Supply(End): accepted len 10 hex 780000000500000001e0 + Supply(Continue): accepted len 12 hex 67000000070000000341e0f6 + Supply(End): accepted len 12 hex 78000000070000000341e0f6 End(Reply): accepted len 1 hex 89 End(Stream): accepted len 1 hex 9a stream 02 @@ -220,8 +220,8 @@ Responder QueryEmpty(End): accepted len 1 hex 35 Query(Continue): accepted len 27 hex 460000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 570000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 680000000500000001e0 - Supply(End): accepted len 10 hex 790000000500000001e0 + Supply(Continue): accepted len 12 hex 68000000070000000341e0f6 + Supply(End): accepted len 12 hex 79000000070000000341e0f6 End(Reply): accepted len 1 hex 8a End(Stream): accepted len 1 hex 9b stream 03 @@ -231,8 +231,8 @@ Responder QueryEmpty(End): accepted len 1 hex 36 Query(Continue): accepted len 27 hex 470000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 580000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 690000000500000001e0 - Supply(End): accepted len 10 hex 7a0000000500000001e0 + Supply(Continue): accepted len 12 hex 69000000070000000341e0f6 + Supply(End): accepted len 12 hex 7a000000070000000341e0f6 End(Reply): accepted len 1 hex 8b End(Stream): accepted len 1 hex 9c stream 04 @@ -242,8 +242,8 @@ Responder QueryEmpty(End): accepted len 1 hex 37 Query(Continue): accepted len 27 hex 480000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 590000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 6a0000000500000001e0 - Supply(End): accepted len 10 hex 7b0000000500000001e0 + Supply(Continue): accepted len 12 hex 6a000000070000000341e0f6 + Supply(End): accepted len 12 hex 7b000000070000000341e0f6 End(Reply): accepted len 1 hex 8c End(Stream): accepted len 1 hex 9d stream 05 @@ -253,8 +253,8 @@ Responder QueryEmpty(End): accepted len 1 hex 38 Query(Continue): accepted len 27 hex 490000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 5a0000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 6b0000000500000001e0 - Supply(End): accepted len 10 hex 7c0000000500000001e0 + Supply(Continue): accepted len 12 hex 6b000000070000000341e0f6 + Supply(End): accepted len 12 hex 7c000000070000000341e0f6 End(Reply): accepted len 1 hex 8d End(Stream): accepted len 1 hex 9e stream 06 @@ -264,8 +264,8 @@ Responder QueryEmpty(End): accepted len 1 hex 39 Query(Continue): accepted len 27 hex 4a0000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 5b0000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 6c0000000500000001e0 - Supply(End): accepted len 10 hex 7d0000000500000001e0 + Supply(Continue): accepted len 12 hex 6c000000070000000341e0f6 + Supply(End): accepted len 12 hex 7d000000070000000341e0f6 End(Reply): accepted len 1 hex 8e End(Stream): accepted len 1 hex 9f stream 07 @@ -275,8 +275,8 @@ Responder QueryEmpty(End): accepted len 1 hex 3a Query(Continue): accepted len 27 hex 4b0000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 5c0000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 6d0000000500000001e0 - Supply(End): accepted len 10 hex 7e0000000500000001e0 + Supply(Continue): accepted len 12 hex 6d000000070000000341e0f6 + Supply(End): accepted len 12 hex 7e000000070000000341e0f6 End(Reply): accepted len 1 hex 8f End(Stream): accepted len 1 hex a0 stream 08 @@ -286,8 +286,8 @@ Responder QueryEmpty(End): accepted len 1 hex 3b Query(Continue): accepted len 27 hex 4c0000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 5d0000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 6e0000000500000001e0 - Supply(End): accepted len 10 hex 7f0000000500000001e0 + Supply(Continue): accepted len 12 hex 6e000000070000000341e0f6 + Supply(End): accepted len 12 hex 7f000000070000000341e0f6 End(Reply): accepted len 1 hex 90 End(Stream): accepted len 1 hex a1 stream 09 @@ -297,8 +297,8 @@ Responder QueryEmpty(End): accepted len 1 hex 3c Query(Continue): accepted len 27 hex 4d0000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 5e0000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 6f0000000500000001e0 - Supply(End): accepted len 10 hex 800000000500000001e0 + Supply(Continue): accepted len 12 hex 6f000000070000000341e0f6 + Supply(End): accepted len 12 hex 80000000070000000341e0f6 End(Reply): accepted len 1 hex 91 End(Stream): accepted len 1 hex a2 stream 10 @@ -308,8 +308,8 @@ Responder QueryEmpty(End): accepted len 1 hex 3d Query(Continue): accepted len 27 hex 4e0000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 5f0000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 700000000500000001e0 - Supply(End): accepted len 10 hex 810000000500000001e0 + Supply(Continue): accepted len 12 hex 70000000070000000341e0f6 + Supply(End): accepted len 12 hex 81000000070000000341e0f6 End(Reply): accepted len 1 hex 92 End(Stream): accepted len 1 hex a3 stream 11 @@ -319,8 +319,8 @@ Responder QueryEmpty(End): accepted len 1 hex 3e Query(Continue): accepted len 27 hex 4f0000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 600000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 710000000500000001e0 - Supply(End): accepted len 10 hex 820000000500000001e0 + Supply(Continue): accepted len 12 hex 71000000070000000341e0f6 + Supply(End): accepted len 12 hex 82000000070000000341e0f6 End(Reply): accepted len 1 hex 93 End(Stream): accepted len 1 hex a4 stream 12 @@ -330,8 +330,8 @@ Responder QueryEmpty(End): accepted len 1 hex 3f Query(Continue): accepted len 27 hex 500000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 610000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 720000000500000001e0 - Supply(End): accepted len 10 hex 830000000500000001e0 + Supply(Continue): accepted len 12 hex 72000000070000000341e0f6 + Supply(End): accepted len 12 hex 83000000070000000341e0f6 End(Reply): accepted len 1 hex 94 End(Stream): accepted len 1 hex a5 stream 13 @@ -341,8 +341,8 @@ Responder QueryEmpty(End): accepted len 1 hex 40 Query(Continue): accepted len 27 hex 510000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 620000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 730000000500000001e0 - Supply(End): accepted len 10 hex 840000000500000001e0 + Supply(Continue): accepted len 12 hex 73000000070000000341e0f6 + Supply(End): accepted len 12 hex 84000000070000000341e0f6 End(Reply): accepted len 1 hex 95 End(Stream): accepted len 1 hex a6 stream 14 @@ -352,8 +352,8 @@ Responder QueryEmpty(End): accepted len 1 hex 41 Query(Continue): accepted len 27 hex 520000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 630000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 740000000500000001e0 - Supply(End): accepted len 10 hex 850000000500000001e0 + Supply(Continue): accepted len 12 hex 74000000070000000341e0f6 + Supply(End): accepted len 12 hex 85000000070000000341e0f6 End(Reply): accepted len 1 hex 96 End(Stream): accepted len 1 hex a7 stream 15 @@ -363,8 +363,8 @@ Responder QueryEmpty(End): accepted len 1 hex 42 Query(Continue): accepted len 27 hex 530000000000000000000000000000000000000000000000000000 Query(End): accepted len 27 hex 640000000000000000000000000000000000000000000000000000 - Supply(Continue): accepted len 10 hex 750000000500000001e0 - Supply(End): accepted len 10 hex 860000000500000001e0 + Supply(Continue): accepted len 12 hex 75000000070000000341e0f6 + Supply(End): accepted len 12 hex 86000000070000000341e0f6 End(Reply): accepted len 1 hex 97 End(Stream): accepted len 1 hex a8 stream 16 @@ -375,6 +375,6 @@ Responder Query(Continue): rejected byte 54 class TerminalLeafReplies Query(End): rejected byte 65 class TerminalLeafReplies Supply(Continue): rejected byte 76 class TerminalLeafReplies - Supply(End): accepted len 10 hex 870000000500000001e0 + Supply(End): accepted len 12 hex 87000000070000000341e0f6 End(Reply): accepted len 1 hex 98 End(Stream): accepted len 1 hex a9 diff --git a/src/tree/mirror/streaming/remote/codec/tests.rs b/src/tree/mirror/streaming/remote/codec/tests.rs index 0d842b8d0..8b7890835 100644 --- a/src/tree/mirror/streaming/remote/codec/tests.rs +++ b/src/tree/mirror/streaming/remote/codec/tests.rs @@ -22,6 +22,7 @@ use crate::{ }, }; +use serde::Serialize; mod error_atlas; /// Largest query fan in the exhaustive small-scope enumeration. @@ -66,7 +67,7 @@ const MAX_ARBITRARY_SUFFIX_LEN: usize = 32; const MAX_ARBITRARY_RUN_RECORDS: usize = 4; /// Build a supply run from decoded leaf records. -fn leaf_run(records: &[(Version, T)]) -> LeafRun { +fn leaf_run(records: &[(Version, T)]) -> LeafRun { let mut run = LeafRun::new(); for (version, value) in records { run.push(version, &Message::new(value.clone())) diff --git a/src/tree/mirror/streaming/remote/codec/tests/error_atlas.rs b/src/tree/mirror/streaming/remote/codec/tests/error_atlas.rs index 3d74c5509..2929a65c0 100644 --- a/src/tree/mirror/streaming/remote/codec/tests/error_atlas.rs +++ b/src/tree/mirror/streaming/remote/codec/tests/error_atlas.rs @@ -19,7 +19,6 @@ use std::{ task::{Context, Poll}, }; -use borsh::BorshSerialize; use tokio::io::AsyncWrite; use super::super::{ @@ -31,6 +30,7 @@ use super::super::{ }; use crate::{Version, message::Message, tree::typed::Hash}; +use serde::Serialize; /// One rendered marker per error variant the atlas must witness. /// /// Grouped by the enum whose `describe_*` match is the compile-time @@ -82,7 +82,7 @@ const INTERIOR_STREAM: u8 = 8; const FIRST_RESERVED_SIGNAL: u8 = WireSignal::BYTE_COUNT; /// Build a supply run holding one leaf record. -fn one_record_run(version: Version, value: T) -> LeafRun { +fn one_record_run(version: Version, value: T) -> LeafRun { let mut run = LeafRun::new(); run.push(&version, &Message::new(value)) .expect("an atlas record fits the run framing"); @@ -314,13 +314,13 @@ fn record_errors(atlas: &mut String) { // A record ending after its version fails at the message decoder. let mut version = Vec::new(); - Version::new().serialize(&mut version).unwrap(); + ciborium::ser::into_writer(&Version::new(), &mut version).unwrap(); let run = LeafRun::::from_encoded(framed_record(&version)).unwrap(); record_leaf(atlas, "record/message", &next_record_error(&run)); // Bytes past the canonical pair are trailing. let mut padded = version.clone(); - 0_u64.serialize(&mut padded).unwrap(); + ciborium::ser::into_writer(&0_u64, &mut padded).unwrap(); padded.push(u8::MIN); let run = LeafRun::::from_encoded(framed_record(&padded)).unwrap(); record_leaf(atlas, "record/trailing", &next_record_error(&run)); @@ -530,7 +530,7 @@ impl FailAfterWriter { } } -impl borsh::io::Write for FailAfterWriter { +impl std::io::Write for FailAfterWriter { fn write(&mut self, bytes: &[u8]) -> io::Result { if self.remaining == 0 { return Err(io::ErrorKind::Other.into()); @@ -581,7 +581,7 @@ impl FailAfterReader { } } -impl borsh::io::Read for FailAfterReader { +impl std::io::Read for FailAfterReader { fn read(&mut self, out: &mut [u8]) -> io::Result { if self.remaining == 0 { return Err(io::ErrorKind::Other.into()); diff --git a/src/tree/mirror/streaming/remote/codec/tests/snapshots/rumors__tree__mirror__streaming__remote__codec__tests__error_atlas__codec_error_atlas_snapshot.snap b/src/tree/mirror/streaming/remote/codec/tests/snapshots/rumors__tree__mirror__streaming__remote__codec__tests__error_atlas__codec_error_atlas_snapshot.snap index 2ee574697..0b652e48f 100644 --- a/src/tree/mirror/streaming/remote/codec/tests/snapshots/rumors__tree__mirror__streaming__remote__codec__tests__error_atlas__codec_error_atlas_snapshot.snap +++ b/src/tree/mirror/streaming/remote/codec/tests/snapshots/rumors__tree__mirror__streaming__remote__codec__tests__error_atlas__codec_error_atlas_snapshot.snap @@ -163,10 +163,10 @@ DECODE kind: InvalidRun::TruncatedRecord(len=2, remaining=1) source[0]: a leaf record of 2 bytes overruns the 1 bytes left in its run Initiator/run/overbatched - display: Initiator stream 8: supply frame occupies 17 wire bytes, batching records past the 0-byte run budget + display: Initiator stream 8: supply frame occupies 19 wire bytes, batching records past the 0-byte run budget origin: Initiator stream 8 - kind: OverbatchedRun(declared=17, budget=0) - source[0]: supply frame occupies 17 wire bytes, batching records past the 0-byte run budget + kind: OverbatchedRun(declared=19, budget=0) + source[0]: supply frame occupies 19 wire bytes, batching records past the 0-byte run budget Initiator/frame/trailing display: Initiator stream 8: 1 trailing bytes follow the frame origin: Initiator stream 8 @@ -259,10 +259,10 @@ DECODE kind: InvalidRun::TruncatedRecord(len=2, remaining=1) source[0]: a leaf record of 2 bytes overruns the 1 bytes left in its run Responder/run/overbatched - display: Responder stream 8: supply frame occupies 17 wire bytes, batching records past the 0-byte run budget + display: Responder stream 8: supply frame occupies 19 wire bytes, batching records past the 0-byte run budget origin: Responder stream 8 - kind: OverbatchedRun(declared=17, budget=0) - source[0]: supply frame occupies 17 wire bytes, batching records past the 0-byte run budget + kind: OverbatchedRun(declared=19, budget=0) + source[0]: supply frame occupies 19 wire bytes, batching records past the 0-byte run budget Responder/frame/trailing display: Responder stream 8: 1 trailing bytes follow the frame origin: Responder stream 8 @@ -290,8 +290,8 @@ RECORD source[0]: Io(UnexpectedEof) record/message display: supplied Message could not be decoded - kind: Record::Message(io=InvalidData) - source[0]: Io(InvalidData) + kind: Record::Message(io=UnexpectedEof) + source[0]: Io(UnexpectedEof) record/trailing display: 1 trailing bytes follow the supplied Version and Message kind: Record::TrailingBytes(count=1) diff --git a/src/tree/mirror/streaming/remote/proxy/start.rs b/src/tree/mirror/streaming/remote/proxy/start.rs index 3080aba6f..ec4868000 100644 --- a/src/tree/mirror/streaming/remote/proxy/start.rs +++ b/src/tree/mirror/streaming/remote/proxy/start.rs @@ -3,7 +3,6 @@ use std::io; use std::marker::PhantomData; -use borsh::BorshDeserialize; use tokio::io::{AsyncRead, AsyncWrite}; use crate::{ @@ -30,11 +29,13 @@ use crate::{ }, typed::{ Hash, + hash::MERKLE_HASH_LEN, height::{Root, Z}, }, }, }; +use serde::de::DeserializeOwned; /// A wire-bound protocol participant ready for the version handshake. /// /// Consumes a [`Link`] carrier for one session: the control halves host the @@ -120,7 +121,7 @@ where impl Connect for Handshaking where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, C: Connector, @@ -147,7 +148,7 @@ where impl CompleteConnect for Handshaking where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, C: Connector, @@ -181,7 +182,7 @@ where impl Accept for Handshaking where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, C: Connector, @@ -246,7 +247,14 @@ where first.extend_from_slice(&greeting.target_message_size.to_le_bytes()); first.extend_from_slice(greeting.version.as_bytes()); write.frame(&first).await.map_err(Error::HandshakeWrite)?; - let listing = borsh::to_vec(&greeting.listing).map_err(Error::HandshakeWrite)?; + // The listing frame is raw fixed-width records — radix byte, then the + // Merkle hash — with the frame length carrying the count, exactly the + // codec's query-listing shape. + let mut listing = Vec::with_capacity(greeting.listing.len() * (1 + MERKLE_HASH_LEN)); + for (radix, hash) in &greeting.listing { + listing.push(*radix); + listing.extend_from_slice(hash.as_bytes()); + } write.frame(&listing).await.map_err(Error::HandshakeWrite) } @@ -270,10 +278,26 @@ where }; let (set_len, max_version_bytes, target_message_size) = framing::greeting_words(&bytes).ok_or_else(short)?; - let version = Version::try_from_slice(&bytes[framing::GREETING_SIZE_WORDS_LEN..]) - .map_err(Error::HandshakeDecode)?; + let version = Version::decode(&bytes[framing::GREETING_SIZE_WORDS_LEN..]) + .map_err(|e| Error::HandshakeDecode(io::Error::new(io::ErrorKind::InvalidData, e)))?; let bytes = read.frame().await.map_err(Error::HandshakeRead)?; - let listing = Vec::<(u8, Hash)>::try_from_slice(&bytes).map_err(Error::HandshakeDecode)?; + // The frame length carries the record count; a remainder is a + // malformed listing, not a short read. + if !bytes.len().is_multiple_of(1 + MERKLE_HASH_LEN) { + return Err(Error::HandshakeDecode(io::Error::new( + io::ErrorKind::InvalidData, + "listing frame is not a whole number of radix-hash records", + ))); + } + let listing: Vec<(u8, Hash)> = bytes + .chunks_exact(1 + MERKLE_HASH_LEN) + .map(|record| { + let (&radix, hash) = record.split_first().expect("a record has a radix byte"); + let mut bytes = [0u8; MERKLE_HASH_LEN]; + bytes.copy_from_slice(hash); + (radix, Hash(bytes)) + }) + .collect(); validate_children(&listing).map_err(Error::HandshakeListing)?; Ok(Greeting { version, @@ -299,7 +323,7 @@ fn connected( ) -> Connected where B: Backend: Leaf>, - T: BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, C: Connector, A: Acceptor, { @@ -354,7 +378,7 @@ fn open( ) -> Connected where B: Backend: Leaf>, - T: BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, C: Connector, A: Acceptor, { diff --git a/src/tree/mirror/streaming/remote/proxy/start/tests.rs b/src/tree/mirror/streaming/remote/proxy/start/tests.rs index 98f792fde..8e3e0211e 100644 --- a/src/tree/mirror/streaming/remote/proxy/start/tests.rs +++ b/src/tree/mirror/streaming/remote/proxy/start/tests.rs @@ -62,6 +62,17 @@ fn ticked_version() -> Version { version } +/// Encode a root-fan listing as its wire form: raw radix-hash records, +/// the frame length carrying the count. +fn encode_listing(children: &[(u8, Hash)]) -> Vec { + let mut body = Vec::new(); + for (radix, hash) in children { + body.push(*radix); + body.extend_from_slice(hash.as_bytes()); + } + body +} + /// A greeting cut inside the version frame's length header fails as a typed /// read error. /// @@ -178,7 +189,7 @@ proptest! { /// /// Both frames are honestly sized around arbitrary bodies, so the fuzz /// lands on the body decoders (the version's bit codec, the listing's - /// borsh shape and order check) rather than on the allocator via a lied + /// record shape and order check) rather than on the allocator via a lied /// length header — the header lies are pinned deterministically above. /// Every outcome must be `Ok` or one of the three typed greeting /// errors. @@ -209,7 +220,7 @@ proptest! { #[pollster::test] async fn unordered_listing_is_rejected() { let listing = vec![(2_u8, Hash::default()), (1_u8, Hash::default())]; - let body = borsh::to_vec(&listing).expect("test listings encode"); + let body = encode_listing(&listing); let result = receive_greeting(&greeting(&body)).await.map(|_| ()); assert!( @@ -232,7 +243,7 @@ async fn unordered_listing_is_rejected() { #[pollster::test] async fn duplicate_listing_radix_is_rejected() { let listing = vec![(3_u8, Hash::default()), (3_u8, Hash::default())]; - let body = borsh::to_vec(&listing).expect("test listings encode"); + let body = encode_listing(&listing); let result = receive_greeting(&greeting(&body)).await.map(|_| ()); assert!( @@ -247,7 +258,7 @@ async fn duplicate_listing_radix_is_rejected() { ); } -/// A listing frame whose borsh body is truncated fails as a typed decode +/// A listing frame whose record body is truncated fails as a typed decode /// error. /// /// A frame declaring more listing entries than its body carries must surface @@ -256,7 +267,7 @@ async fn duplicate_listing_radix_is_rejected() { #[pollster::test] async fn truncated_listing_body_is_rejected() { let listing = vec![(0_u8, Hash::default()), (1_u8, Hash::default())]; - let mut body = borsh::to_vec(&listing).expect("test listings encode"); + let mut body = encode_listing(&listing); body.truncate(body.len() - 1); let result = receive_greeting(&greeting(&body)).await.map(|_| ()); @@ -269,14 +280,14 @@ async fn truncated_listing_body_is_rejected() { /// A listing frame with bytes after the listing fails as a typed decode /// error. /// -/// The greeting decode is canonical: the frame must contain exactly one -/// borsh listing, so trailing garbage surfaces [`Error::HandshakeDecode`] -/// rather than being silently ignored (which would let two encodings name -/// one greeting). +/// The greeting decode is canonical: the frame must be a whole number of +/// radix-hash records, so trailing garbage surfaces +/// [`Error::HandshakeDecode`] rather than being silently ignored (which +/// would let two encodings name one greeting). #[pollster::test] async fn trailing_listing_bytes_are_rejected() { let listing: Vec<(u8, Hash)> = Vec::new(); - let mut body = borsh::to_vec(&listing).expect("test listings encode"); + let mut body = encode_listing(&listing); body.push(0xFF); let result = receive_greeting(&greeting(&body)).await.map(|_| ()); @@ -295,7 +306,7 @@ async fn trailing_listing_bytes_are_rejected() { #[pollster::test] async fn empty_listing_greeting_decodes() { let listing: Vec<(u8, Hash)> = Vec::new(); - let body = borsh::to_vec(&listing).expect("test listings encode"); + let body = encode_listing(&listing); let handshake = receive_greeting(&greeting(&body)) .await diff --git a/src/tree/mirror/streaming/remote/proxy/state.rs b/src/tree/mirror/streaming/remote/proxy/state.rs index 85eece3dc..04eab96dd 100644 --- a/src/tree/mirror/streaming/remote/proxy/state.rs +++ b/src/tree/mirror/streaming/remote/proxy/state.rs @@ -25,6 +25,7 @@ use crate::tree::{ typed::height::{Height, Root, S, UnderRoot, UnderUnderRoot, Z}, }; +use serde::de::DeserializeOwned; /// Session endpoints and backend shared by every state in one proxy chain. struct Session where @@ -50,7 +51,7 @@ where impl Session where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, C: Connector, A: Acceptor, { @@ -109,7 +110,7 @@ where impl Connected where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, C: Connector, A: Acceptor, { @@ -232,7 +233,7 @@ where impl protocol::CompleteEqual for Connected where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, C: Connector, @@ -250,7 +251,7 @@ where impl protocol::Initiator for Connected where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, C: Connector, @@ -282,7 +283,7 @@ where impl protocol::Responder for Connected where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, C: Connector, @@ -321,7 +322,7 @@ where impl protocol::Reply for Descending>, R, W, C, A> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, C: Connector, @@ -357,7 +358,7 @@ where impl protocol::Reply for Descending, R, W, C, A> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, C: Connector, @@ -391,7 +392,7 @@ where impl protocol::CompleteInitiator for Completing where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, C: Connector, @@ -414,7 +415,7 @@ where impl protocol::CompleteResponder for Descending where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, C: Connector, diff --git a/src/tree/mirror/streaming/remote/proxy/tests.rs b/src/tree/mirror/streaming/remote/proxy/tests.rs index 3e41f03e3..9c02da448 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests.rs @@ -32,6 +32,7 @@ use crate::tree::{ }; use crate::{Version, message::Message, tree::mirror::Error as MirrorError}; +use serde::de::DeserializeOwned; type BackendFailure = Failure; type LocalFailure = MaterializedError; type ProxyFailure = RemoteError; @@ -72,7 +73,7 @@ async fn reconcile_symmetric_accepts( transport_capacity: usize, ) -> (TreeRoot, TreeRoot) where - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, { let a = Handshaking::start(Local, Root::from(a)).window(WindowConfig::FLOOR); let b = Handshaking::start(Local, Root::from(b)).window(WindowConfig::FLOOR); @@ -103,7 +104,7 @@ async fn reconcile_symmetric_accepts_reordered( reordered: Arc, ) -> (TreeRoot, TreeRoot) where - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, { let a = Handshaking::start(Local, Root::from(a)).window(WindowConfig::FLOOR); let b = Handshaking::start(Local, Root::from(b)).window(WindowConfig::FLOOR); @@ -123,7 +124,7 @@ where /// transport halves, proving that neither phase consumes the other's bytes. async fn reconcile_after_preamble(a: TreeRoot, b: TreeRoot) -> (TreeRoot, TreeRoot) where - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, { let a = Handshaking::start(Local, Root::from(a)).window(WindowConfig::FLOOR); let b = Handshaking::start(Local, Root::from(b)).window(WindowConfig::FLOOR); @@ -270,7 +271,7 @@ async fn equal_versions_return_both_roots() { assert_eq!(b, root); } -/// Concurrent content-addressed leaves cross every proxy layer and converge. +/// Concurrent version-addressed leaves cross every proxy layer and converge. #[pollster::test] async fn divergent_leaves_converge() { let mut a = Tree::new(); diff --git a/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs b/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs index 434ce359a..62bca8ac9 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs @@ -121,8 +121,11 @@ fn empty_carried_listing_asks_for_everything() { // while redaction keeps the tree (and so its advertised set) empty. let mut emptied = Tree::new(); emptied.act(&nth_party(1), [Action::Insert(Message::new(()))]); - let keys: Vec<_> = emptied.iter().map(|(key, _, _)| key).collect(); - emptied.act(&nth_party(1), keys.into_iter().map(Action::Forget)); + let paths: Vec<_> = emptied + .iter() + .map(|(v, _)| crate::tree::typed::Path::for_leaf(v)) + .collect(); + emptied.act(&nth_party(1), paths.into_iter().map(Action::Forget)); assert!(emptied.is_empty(), "the initiator's tree must be empty"); let expected = { @@ -237,14 +240,17 @@ fn overlapping_sessions_lose_innocent_leaf_after_honored_redaction() { let p = nth_party(0); let mut t0 = Tree::new(); t0.act(&p, (0..25).map(|_| Action::Insert(Message::new(())))); - let keys: Vec<_> = t0.iter().map(|(k, _, _)| k).collect(); + let leaves: Vec<_> = t0 + .iter() + .map(|(v, _)| (crate::tree::typed::Path::for_leaf(v), v.clone())) + .collect(); // S2's wire session against a peer converged at T0, forked at T0: // equal versions resolve to the fork-time root. let (s2_reconciled, _) = wire_reconcile(t0.root.clone(), t0.root.clone()); let mut lost = Vec::new(); - for k in &keys { + for (k, _) in &leaves { // S1's counterparty: converged at T0, then redacted the leaf at // `k` (a local act rebuilds its own fans afresh; the sharing that // matters is created by our install below, not here). @@ -273,10 +279,10 @@ fn overlapping_sessions_lose_innocent_leaf_after_honored_redaction() { }); if live.hash() != expected { - let missing: Vec<_> = keys + let missing: Vec<_> = leaves .iter() - .filter(|k2| *k2 != k && live.get(k2).is_none()) - .copied() + .filter(|(k2, v2)| k2 != k && live.get(v2).is_none()) + .map(|(k2, _)| *k2) .collect(); lost.push((*k, missing)); } diff --git a/src/tree/mirror/streaming/remote/proxy/work.rs b/src/tree/mirror/streaming/remote/proxy/work.rs index 31b45a182..9b7e63ccb 100644 --- a/src/tree/mirror/streaming/remote/proxy/work.rs +++ b/src/tree/mirror/streaming/remote/proxy/work.rs @@ -32,6 +32,7 @@ use crate::tree::{ use self::progress::Progress; +use serde::de::DeserializeOwned; mod encode; pub(super) mod progress; mod pump; @@ -88,7 +89,7 @@ where impl Work where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, A: Acceptor, { /// Begin accumulating work around an elected physical session. diff --git a/src/tree/mirror/streaming/remote/proxy/work/pump.rs b/src/tree/mirror/streaming/remote/proxy/work/pump.rs index b352cebcc..0e028cf5c 100644 --- a/src/tree/mirror/streaming/remote/proxy/work/pump.rs +++ b/src/tree/mirror/streaming/remote/proxy/work/pump.rs @@ -51,10 +51,11 @@ use crate::tree::{ use super::{encode, queues}; +use serde::de::DeserializeOwned; impl Work where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, A: Acceptor, @@ -367,7 +368,7 @@ where impl Early where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, G: Convert, S: Height, Rx: tokio::io::AsyncRead + Unpin + Send + 'static, @@ -465,7 +466,7 @@ where async fn reject_extra(incoming: &mut StreamReceiver) -> Result<(), Error> where Rx: tokio::io::AsyncRead + Unpin + Send + 'static, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, { match incoming.finish().await { ReceiverFinish::Clean => Ok(()), diff --git a/src/tree/mirror/streaming/remote/streams.rs b/src/tree/mirror/streaming/remote/streams.rs index 19d76d520..6d88a50b3 100644 --- a/src/tree/mirror/streaming/remote/streams.rs +++ b/src/tree/mirror/streaming/remote/streams.rs @@ -40,7 +40,6 @@ use std::pin::Pin; use std::task::{Context, Poll}; use async_stream::stream; -use borsh::BorshDeserialize; use futures::{StreamExt, stream::BoxStream}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, @@ -55,6 +54,7 @@ use super::codec::{ DecodeError, EncodeError, End, Frame, FrameRead, FrameWrite, Origin, RunBudget, Speaker, Stream, }; +use serde::de::DeserializeOwned; /// Bytes of the label a sender writes before its first frame. /// /// The canonical definition of the wire label's width: the capture @@ -312,7 +312,7 @@ struct ReceiverStart { impl StreamReceiver where Rx: tokio::io::AsyncRead + Unpin + Send + 'static, - T: BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, { /// Bind one incoming logical stream to its claim slot. pub fn new( @@ -383,7 +383,7 @@ pub enum ReceiverFinish { impl futures::Stream for StreamReceiver where Rx: tokio::io::AsyncRead + Unpin + Send + 'static, - T: BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, { type Item = Frame; @@ -406,7 +406,7 @@ fn read_frames( ) -> impl futures::Stream> + Send where Rx: tokio::io::AsyncRead + Unpin + Send + 'static, - T: BorshDeserialize + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, { stream! { let Ok((rx, done)) = claim.await else { diff --git a/src/tree/mirror/streaming/tests/fixtures.rs b/src/tree/mirror/streaming/tests/fixtures.rs index f7613fa28..d81c6ef31 100644 --- a/src/tree/mirror/streaming/tests/fixtures.rs +++ b/src/tree/mirror/streaming/tests/fixtures.rs @@ -1,7 +1,6 @@ //! Deterministic tree shapes for streaming integration, capacity, and //! skeleton-bridge tests. -use borsh::BorshSerialize; use proptest::prelude::*; use crate::{ @@ -15,6 +14,7 @@ use crate::{ }, }; +use serde::Serialize; /// A 32-byte path with the given prefix, zero-padded. pub(super) fn path_at(prefix: &[u8]) -> Path { let mut bytes = [0u8; 32]; @@ -39,7 +39,7 @@ pub(super) fn grown( paths: &[Path], ) -> Option> where - T: BorshSerialize + Clone + Send + Sync, + T: Serialize + Clone + Send + Sync, { assert!(stride > 0, "each leaf needs a fresh version"); let party = nth_party(party); @@ -163,20 +163,27 @@ impl CellSpec { } impl Divergence { - /// The shared leaves' paths, in cell order. + /// The shared leaves' paths, in cell order, first occurrence per path: + /// sampled cells may repeat a prefix, and one path is one leaf — an + /// insert never lands on an occupied path. pub fn shared_paths(&self) -> Vec<[u8; 32]> { + let mut seen = std::collections::BTreeSet::new(); self.cells .iter() .flat_map(|cell| (0..cell.shared).map(|i| cell.path(SHARED_SLOT, i))) + .filter(|path| seen.insert(*path)) .collect() } - /// The local tree's one-sided extras. + /// The local tree's one-sided extras, deduplicated as + /// [`shared_paths`](Self::shared_paths) does. pub fn local_paths(&self) -> Vec<[u8; 32]> { + let mut seen = std::collections::BTreeSet::new(); self.cells .iter() .filter(|cell| cell.local) .map(|cell| cell.path(LOCAL_SLOT, 0)) + .filter(|path| seen.insert(*path)) .collect() } @@ -231,7 +238,7 @@ impl Divergence { /// in both. pub fn trees(&self, value: &T) -> (Root, Root, Root) where - T: BorshSerialize + Clone + Send + Sync, + T: Serialize + Clone + Send + Sync, { let as_paths = |bytes: Vec<[u8; 32]>| -> Vec { bytes.into_iter().map(Path::from).collect() }; diff --git a/src/tree/mirror/streaming/tests/wedge.rs b/src/tree/mirror/streaming/tests/wedge.rs index cc41e3557..066a6efdd 100644 --- a/src/tree/mirror/streaming/tests/wedge.rs +++ b/src/tree/mirror/streaming/tests/wedge.rs @@ -11,7 +11,7 @@ //! //! On the committed seeds: `tests/pairwise.proptest-regressions` and //! `tests/shadow_validity.proptest-regressions` are integration-level -//! seeds (three-peer networks, content-addressed keys, whole-`Rumors` +//! seeds (three-peer networks, version-addressed leaves, whole-`Rumors` //! action lists) that realize the wedge's *jam mechanism*, not its //! byte-exact shape; a structural equality pin needs hand-placed paths. //! This bridge therefore constructs the pair deterministically and pins diff --git a/src/tree/mirror/streaming/window.rs b/src/tree/mirror/streaming/window.rs index 3655a769b..e06bf9310 100644 --- a/src/tree/mirror/streaming/window.rs +++ b/src/tree/mirror/streaming/window.rs @@ -1,5 +1,5 @@ //! The pipeline window: per-height static bounds on in-flight disputed -//! scopes, sized by the occupancy statistics of uniform content addresses. +//! scopes, sized by the occupancy statistics of uniform leaf paths. //! //! Every recursive edge in the streaming session — the walk's query and //! resolution queues, the proxy's flushed-question and next-scope queues — @@ -131,7 +131,7 @@ use crate::tree::typed::{self, Prefix, height::Z}; /// regardless of any window tuning. pub(crate) const FAN: usize = 256; -/// Radix levels in the trie: one byte of a 32-byte content address per +/// Radix levels in the trie: one byte of a 32-byte leaf path per /// level. Typed heights run from `Z = 0` (leaves) to `Root = KEY_DEPTH`; /// the *depth* of the children discussed at height `h` is `KEY_DEPTH − h`. const KEY_DEPTH: usize = 32; @@ -201,18 +201,22 @@ pub(crate) const SUPPLY_DECODE_ENVELOPE_BYTES: usize = pub(crate) const SPEC_BDP_BYTES: usize = 12_500_000; /// End-to-end wire bytes of one disputed message beyond its record's -/// encoded payload: its question share, reply share, and record framing. +/// encoded payload. +/// +/// Its question share, reply share, and record framing (the record's +/// version atom rides as a CBOR byte string, whose header is part of +/// this intercept). /// /// Calibrated: `tests/dispute_wire.rs` counts every byte of /// deterministic in-memory sessions and pins the per-message cost as an -/// affine law — this intercept plus the record's borsh-encoded +/// affine law — this intercept plus the record's CBOR-encoded /// payload — at three payload sizes. The closed form documented at /// [`Peer::sync_memory_budget`](crate::Peer::sync_memory_budget) is /// denominated in it. #[cfg(any(test, feature = "test-internals"))] -pub(crate) const DISPUTE_OVERHEAD_BYTES: usize = 34; +pub(crate) const DISPUTE_OVERHEAD_BYTES: usize = 35; -/// The design record's borsh-encoded payload size: the `m = 172` column +/// The design record's CBOR-encoded payload size: the `m = 172` column /// of the trade-off table, and the record size the wire-cost anchor /// below is stated at. #[cfg(any(test, feature = "test-internals"))] @@ -549,7 +553,7 @@ impl Default for WindowConfig { // ─── The integer occupancy envelopes ───────────────────────────────────── // -// Uniform 32-byte content addresses put Binomial(N, 256⁻ʲ) leaves under +// Uniform 32-byte leaf paths put Binomial(N, 256⁻ʲ) leaves under // each depth-j prefix, with iid-uniform continuations — exact, no // Poissonization. Chernoff–Hoeffding tails apply verbatim to every // count below even though slot occupancies are dependent: occupancy diff --git a/src/tree/mirror/streaming/window/tests.rs b/src/tree/mirror/streaming/window/tests.rs index 06bc06449..e1fde7930 100644 --- a/src/tree/mirror/streaming/window/tests.rs +++ b/src/tree/mirror/streaming/window/tests.rs @@ -266,11 +266,11 @@ fn tradeoff_table_matches_the_derivation() { /// The crossover and BDP-scale u64 figures the docs quote are the /// solve's own numbers. /// -/// `m* = 61 B` (quoted at `Peer::sync_memory_budget`) is the +/// `m* = 60 B` (quoted at `Peer::sync_memory_budget`) is the /// smallest record size whose self-consistent corpus — the spec BDP in /// `m`-size records, per side — fits entirely inside the window the /// default budget derives at that corpus; the u64 column's BDP-scale -/// corpus derives a 65,401-scope window, the quoted ~4.6× figure. +/// corpus derives a 65,404-scope window, the quoted ~4.3× figure. /// Both are recomputed here from the derivation, so the quoted prose /// fails loudly instead of drifting when the solve or its constants /// change. @@ -296,14 +296,15 @@ fn default_crossover_matches_the_solve() { }); assert_eq!( crossover, - Some(61), + Some(60), "the default's self-consistent slowdown-1 crossover moved: update the figures \ quoted at Peer::sync_memory_budget", ); - let u64_corpus = (SPEC_BDP_BYTES / (DISPUTE_OVERHEAD_BYTES + 8)) as u64; + // A random u64 payload CBOR-encodes to 9 bytes (header + value). + let u64_corpus = (SPEC_BDP_BYTES / (DISPUTE_OVERHEAD_BYTES + 9)) as u64; assert_eq!( window_at(u64_corpus), - 65_401, + 65_404, "the u64 BDP-scale window moved: update the ~4.6x figure quoted at \ Peer::sync_memory_budget", ); diff --git a/src/tree/mirror/streaming/window/tradeoff.md b/src/tree/mirror/streaming/window/tradeoff.md index 668d308d8..efafddd5e 100644 --- a/src/tree/mirror/streaming/window/tradeoff.md +++ b/src/tree/mirror/streaming/window/tradeoff.md @@ -1,11 +1,11 @@ -| budget | window (scopes) | m = 8 (u64) | m = 64 | m = 172 (design record) | m = 1024 | +| budget | window (scopes) | m = 9 (u64) | m = 64 | m = 172 (design record) | m = 1024 | |---|---|---|---|---|---| -| 256 KiB | 1 | 297619.0× | 127551.0× | 60679.6× | 11814.7× | -| 1 MiB | 30 | 9920.6× | 4251.7× | 2022.7× | 393.8× | -| 4 MiB | 154 | 1932.6× | 828.3× | 394.0× | 76.7× | -| 16 MiB | 1938 | 153.6× | 65.8× | 31.3× | 6.1× | -| 64 MiB | 11019 | 27.0× | 11.6× | 5.5× | 1.1× | -| 256 MiB | 49055 | 6.1× | 2.6× | 1.2× | 1.0× | -| 512 MiB (default) | 62500 | 4.8× | 2.0× | 1.0× | 1.0× | -| 2 GiB | 62500 | 4.8× | 2.0× | 1.0× | 1.0× | +| 256 KiB | 1 | 284090.9× | 126262.6× | 60386.5× | 11803.6× | +| 1 MiB | 30 | 9469.7× | 4208.8× | 2012.9× | 393.5× | +| 4 MiB | 154 | 1844.7× | 819.9× | 392.1× | 76.6× | +| 16 MiB | 1938 | 146.6× | 65.2× | 31.2× | 6.1× | +| 64 MiB | 11019 | 25.8× | 11.5× | 5.5× | 1.1× | +| 256 MiB | 49055 | 5.8× | 2.6× | 1.2× | 1.0× | +| 512 MiB (default) | 62500 | 4.5× | 2.0× | 1.0× | 1.0× | +| 2 GiB | 62500 | 4.5× | 2.0× | 1.0× | 1.0× | diff --git a/src/tree/tests.rs b/src/tree/tests.rs index 1963cfa75..60b7491ac 100644 --- a/src/tree/tests.rs +++ b/src/tree/tests.rs @@ -3,17 +3,14 @@ use std::collections::{BTreeSet, HashMap}; use bytes::Bytes; use proptest::prelude::*; -use super::typed::{Hash, Path, hash::Hasher, untyped}; +use super::typed::{Hash, Path, untyped}; use super::*; use crate::message::Message; -impl Arbitrary for Key { - type Parameters = (); - type Strategy = BoxedStrategy; - - fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { - any::<[u8; 32]>().prop_map(Key).boxed() - } +use serde::Serialize; +/// An arbitrary 32-byte leaf path (almost surely naming no live leaf). +fn arb_path() -> impl Strategy { + any::<[u8; 32]>().prop_map(Path::from) } /// Wrap a `Bytes` value as a `Message` with its cached serialization. @@ -82,15 +79,14 @@ fn version_for(party: impl AsRef<[u8]>, ticks: u64) -> Version { v } -/// Compute the leaf-path `Key` that `Tree::act` assigns for an insert of -/// `value` at the version a party reaches after `scalar` events. +/// Compute the leaf path that `Tree::act` assigns for an insert at +/// the version a party reaches after `scalar` events. /// -/// The path is derived from the version's canonical bytes (see -/// [`Path::for_leaf`]), and the tree hashes over the *serialized* message -/// bytes, so we feed the cached serialization through. This matches what the -/// tree derives internally for the same post-tick version. -fn leaf_path(party: impl AsRef<[u8]>, scalar: u64, value: &Bytes) -> Key { - Path::for_leaf(&version_for(party, scalar), msg(value.clone()).bytes()).into() +/// The path is derived from the version's canonical bytes alone (see +/// [`Path::for_leaf`]), matching what the tree derives internally for the +/// same post-tick version. +fn leaf_path(party: impl AsRef<[u8]>, scalar: u64) -> Path { + Path::for_leaf(&version_for(party, scalar)) } /// Build a versioned insert triple of the shape `Tree::react` expects: @@ -104,8 +100,8 @@ fn insert_at( party: impl AsRef<[u8]>, scalar: u64, value: Bytes, -) -> (Key, Version, Message) { - (leaf_path(party, scalar, &value), version, msg(value)) +) -> (Path, Version, Message) { + (leaf_path(party, scalar), version, msg(value)) } /// Compute the root hash of the canonical maximally-compressed trie over the @@ -113,12 +109,12 @@ fn insert_at( /// ground truth. /// /// The canonical shape is derived directly from the sorted leaf-path set: a -/// lone path below `depth` is a leaf committing its remaining suffix, and -/// otherwise the run's shared span up to its first divergence byte is the -/// branch's compressed prefix, with one child recursing per divergence -/// radix — so every branch has >= 2 children and maximal prefixes by -/// construction. Preimages are assembled with literal tag bytes, -/// `LEAF_TAG ‖ len ‖ suffix` and +/// lone path below `depth` is a leaf committing its remaining suffix and its +/// version's canonical bytes, and otherwise the run's shared span up to its +/// first divergence byte is the branch's compressed prefix, with one child +/// recursing per divergence radix — so every branch has >= 2 children and +/// maximal prefixes by construction. Preimages are assembled with literal +/// tag bytes, `LEAF_TAG ‖ len ‖ suffix` and /// `BRANCH_TAG ‖ len ‖ prefix ‖ count(u16 BE) ‖ (radix ‖ hash)*`, each hash /// truncated to its leading /// [`MERKLE_HASH_LEN`](crate::tree::typed::hash::MERKLE_HASH_LEN) bytes. The @@ -128,67 +124,60 @@ fn reference_hash(values: &[(Version, Bytes)]) -> Hash { const LEAF_TAG: u8 = 0; const BRANCH_TAG: u8 = 1; - fn hash_at(depth: usize, paths: &[[u8; 32]]) -> Hash { - if let [path] = paths { - let mut hasher = Hasher::new(); - hasher.update(&[LEAF_TAG, (32 - depth) as u8]); - hasher.update(&path[depth..]); - return hasher.finalize().truncate(); + fn hash_at(depth: usize, leaves: &[([u8; 32], &Version)]) -> Hash { + if let [(path, _version)] = leaves { + let mut preimage = vec![LEAF_TAG, (32 - depth) as u8]; + preimage.extend_from_slice(&path[depth..]); + return Hash::of(&preimage); } // Two or more distinct sorted paths diverge at the first byte where // the least and greatest differ; the span from `depth` up to that // byte is the branch's compressed prefix. - let first = paths.first().expect("a run is non-empty"); - let last = paths.last().expect("a run is non-empty"); + let (first, _) = leaves.first().expect("a run is non-empty"); + let (last, _) = leaves.last().expect("a run is non-empty"); let branch_at = (depth..32) .find(|&at| first[at] != last[at]) .expect("distinct 32-byte paths diverge before the bottom"); let mut records: Vec<(u8, Hash)> = Vec::new(); - let mut rest = paths; - while let Some(radix) = rest.first().map(|path| path[branch_at]) { + let mut rest = leaves; + while let Some(radix) = rest.first().map(|(path, _)| path[branch_at]) { let split = rest .iter() - .position(|path| path[branch_at] != radix) + .position(|(path, _)| path[branch_at] != radix) .unwrap_or(rest.len()); let (group, tail) = rest.split_at(split); records.push((radix, hash_at(branch_at + 1, group))); rest = tail; } - let mut hasher = Hasher::new(); - hasher.update(&[BRANCH_TAG, (branch_at - depth) as u8]); - hasher.update(&first[depth..branch_at]); + let mut preimage = vec![BRANCH_TAG, (branch_at - depth) as u8]; + preimage.extend_from_slice(&first[depth..branch_at]); let count = u16::try_from(records.len()).expect("fan-out is at most 256"); - hasher.update(&count.to_be_bytes()); + preimage.extend_from_slice(&count.to_be_bytes()); for (radix, hash) in records { - hasher.update(&[radix]); - hasher.update(hash.as_bytes()); + preimage.push(radix); + preimage.extend_from_slice(hash.as_bytes()); } - hasher.finalize().truncate() + Hash::of(&preimage) } - // Level 32 (the value level): every distinct path maps to a leaf. The tree - // hashes over the serialized `Message` bytes, not the raw inner value, so - // we do the same here. - let paths: BTreeSet = values + // Level 32 (the value level): every distinct path maps to a leaf; the + // path is a pure function of the version. + let mut leaves: Vec<([u8; 32], &Version)> = values .iter() - .map(|(version, value)| Path::for_leaf(version, msg(value.clone()).bytes()).into()) + .map(|(version, _)| (<[u8; 32]>::from(Path::for_leaf(version)), version)) .collect(); + leaves.sort_by_key(|(path, _)| *path); + leaves.dedup_by_key(|(path, _)| *path); - if paths.is_empty() { + if leaves.is_empty() { // The empty tree: a prefixless branch with no children. - let mut hasher = Hasher::new(); - hasher.update(&[BRANCH_TAG, 0, 0, 0]); - return hasher.finalize().truncate(); + return Hash::of(&[BRANCH_TAG, 0, 0, 0]); } - let paths: Vec<[u8; 32]> = paths - .into_iter() - .map(|p| <[u8; 32]>::from(typed::Path::from(p))) - .collect(); - hash_at(0, &paths) + hash_at(0, &leaves) } /// An empty tree's root hash must match the reference: the prefixless branch @@ -265,11 +254,11 @@ proptest! { ) { // One tick of the leaf's own disjoint party; kept leaf indices come // from base order, extras continue the numbering beyond them. - let event = |index: usize, b: &Bytes| -> (Key, Version, Message) { + let event = |index: usize, b: &Bytes| -> (Path, Version, Message) { let mut version = Version::new(); version.tick(&crate::tree::arb::nth_party(index)); let message = msg(b.clone()); - let key = Path::for_leaf(&version, message.bytes()).into(); + let key = Path::for_leaf(&version); (key, version, message) }; let index_of: HashMap = kept @@ -287,7 +276,7 @@ proptest! { // Route B: shuffled order, split into two batches, with the extra // leaves inserted in between and redacted again afterwards. - let extra_events: Vec<(Key, Version, Message)> = extras + let extra_events: Vec<(Path, Version, Message)> = extras .iter() .enumerate() .map(|(i, b)| event(kept.len() + i, b)) @@ -312,7 +301,7 @@ proptest! { tree.root .root .as_ref() - .map(|node| borsh::to_vec(node).expect("node serializes")) + .map(|node| crate::tree::wire::to_vec(node).expect("node serializes")) }; let expected = serialize(&direct); prop_assert_eq!(&serialize(&detoured), &expected); @@ -323,9 +312,9 @@ proptest! { // The canonical bulk construction over the sorted live leaf set. let mut entries: Vec<([u8; 32], Option>)> = direct .iter() - .map(|(key, version, value)| { + .map(|(version, value)| { ( - key.0, + <[u8; 32]>::from(Path::for_leaf(version)), Some(untyped::Node::leaf( version.clone(), Message::new((**value).clone()), @@ -359,25 +348,28 @@ proptest! { breaks in proptest::collection::vec(any::(), 0..16), ) { let party = "P".to_string(); - let version = version_for(&party, 1); + // One fresh scalar per insert, as `act` would assign: each leaf's + // path is its version's, so per-insert versions keep leaves + // distinct however the list is chunked. + let event = |(i, b): (usize, Bytes)| { + let scalar = (i + 1) as u64; + insert_at(version_for(&party, scalar), &party, scalar, b) + }; let mut all_in_one = Tree::new(); - all_in_one.react( - bytes - .iter() - .cloned() - .map(|b| insert_at(version.clone(), &party, 1, b))); + all_in_one + .react(bytes.iter().cloned().enumerate().map(event)); let mut partitioned = Tree::new(); - let mut chunk: Vec = Vec::new(); + let mut chunk: Vec<(usize, Bytes)> = Vec::new(); for (i, b) in bytes.iter().cloned().enumerate() { - chunk.push(b); + chunk.push((i, b)); let at_boundary = breaks.get(i).copied().unwrap_or(false) || i + 1 == bytes.len(); if at_boundary { let batch: Vec<_> = std::mem::take(&mut chunk) .into_iter() - .map(|b| insert_at(version.clone(), &party, 1, b)) + .map(event) .collect(); partitioned.react(batch); } @@ -459,7 +451,7 @@ proptest! { prop_assert_eq!(tree.earliest().is_none(), tree.is_empty()); if let Some(earliest) = tree.earliest() { let latest = tree.latest(); - for (_, v, _) in tree.iter() { + for (v, _) in tree.iter() { prop_assert!(earliest <= v); prop_assert!(v <= latest); } @@ -482,12 +474,14 @@ proptest! { bytes.iter().cloned().map(insert_action)); } - // Forward order is strictly ascending by key. - let fwd: Vec<[u8; 32]> = tree.iter().map(|(k, _, _)| k.0).collect(); + // Forward order is strictly ascending by path. + let fwd: Vec<[u8; 32]> = + tree.iter().map(|(v, _)| <[u8; 32]>::from(Path::for_leaf(v))).collect(); prop_assert!(fwd.windows(2).all(|w| w[0] < w[1])); // Reverse iteration is the forward sequence, reversed. - let bwd: Vec<[u8; 32]> = tree.iter().rev().map(|(k, _, _)| k.0).collect(); + let bwd: Vec<[u8; 32]> = + tree.iter().rev().map(|(v, _)| <[u8; 32]>::from(Path::for_leaf(v))).collect(); let mut fwd_rev = fwd.clone(); fwd_rev.reverse(); prop_assert_eq!(bwd, fwd_rev); @@ -497,8 +491,9 @@ proptest! { let mut it = tree.iter(); let (mut front, mut back) = (Vec::new(), Vec::new()); let mut take_front = true; - while let Some((k, _, _)) = if take_front { it.next() } else { it.next_back() } { - if take_front { front.push(k.0) } else { back.push(k.0) } + while let Some((v, _)) = if take_front { it.next() } else { it.next_back() } { + let path = <[u8; 32]>::from(Path::for_leaf(v)); + if take_front { front.push(path) } else { back.push(path) } take_front = !take_front; } back.reverse(); @@ -517,7 +512,7 @@ proptest! { fn insert_then_delete_is_empty(value in any::>()) { let party = "P".to_string(); let value = Bytes::from(value); - let path = leaf_path(&party, 1, &value); + let path = leaf_path(&party, 1); let mut tree = Tree::new(); tree.act(&party_of("P"), [insert_action(value)]); @@ -535,7 +530,7 @@ proptest! { fn insert_and_delete_same_batch_is_empty(value in any::>()) { let party = "P".to_string(); let value = Bytes::from(value); - let path = leaf_path(&party, 1, &value); + let path = leaf_path(&party, 1); let mut tree = Tree::new(); tree.act(&party_of("P"), [insert_action(value), Action::Forget(path)]); @@ -550,12 +545,11 @@ proptest! { #[test] fn delete_absent_path_preserves_hash( bytes in distinct_bytes(8), - nuke in any::(), + nuke in arb_path(), ) { let party = "P".to_string(); - let present: BTreeSet = bytes - .iter() - .map(|b| leaf_path(&party, 1, b)) + let present: BTreeSet = (1..=bytes.len() as u64) + .map(|scalar| leaf_path(&party, scalar)) .collect(); prop_assume!(!present.contains(&nuke)); @@ -623,50 +617,70 @@ proptest! { /// commute: the order in which the batches are applied does not change /// the resulting tree. /// - /// "Disjoint" here is ensured by giving the two - /// batches different scalar versions, which produces different leaf - /// paths regardless of any overlap in values. + /// "Disjoint" here is ensured by giving every insert its own scalar + /// version, which produces a distinct leaf path per insert. #[test] fn react_commutative( bytes_a in distinct_bytes(8), bytes_b in distinct_bytes(8), ) { let party = "P".to_string(); - let v_a = version_for(&party, 1); - let v_b = version_for(&party, 2); + let batch_a: Vec<_> = bytes_a + .iter() + .cloned() + .enumerate() + .map(|(i, b)| { + let scalar = (i + 1) as u64; + insert_at(version_for(&party, scalar), &party, scalar, b) + }) + .collect(); + let batch_b: Vec<_> = bytes_b + .iter() + .cloned() + .enumerate() + .map(|(i, b)| { + let scalar = (bytes_a.len() + i + 1) as u64; + insert_at(version_for(&party, scalar), &party, scalar, b) + }) + .collect(); let mut t_ab = Tree::new(); - t_ab.react( - bytes_a.iter().cloned().map(|b| insert_at(v_a.clone(), &party, 1, b))); - t_ab.react( - bytes_b.iter().cloned().map(|b| insert_at(v_b.clone(), &party, 2, b))); + t_ab.react(batch_a.clone()); + t_ab.react(batch_b.clone()); let mut t_ba = Tree::new(); - t_ba.react( - bytes_b.iter().cloned().map(|b| insert_at(v_b.clone(), &party, 2, b))); - t_ba.react( - bytes_a.iter().cloned().map(|b| insert_at(v_a.clone(), &party, 1, b))); + t_ba.react(batch_b); + t_ba.react(batch_a); prop_assert_eq!(t_ab, t_ba); } /// `react` is idempotent: applying the same batch twice is identical to - /// applying it once. This is the CRDT property that lets us re-deliver - /// messages safely in the face of retries or out-of-order transport. + /// applying it once. + /// + /// This is the CRDT property that lets us re-deliver messages safely + /// in the face of retries or out-of-order transport, and it rides the + /// identical-leaf arm: a re-delivered insert matches the resident leaf + /// byte-for-byte and is kept, never a collision. #[test] fn react_idempotent(bytes in distinct_bytes(16)) { let party = "P".to_string(); - let v = version_for(&party, 1); + let batch: Vec<_> = bytes + .iter() + .cloned() + .enumerate() + .map(|(i, b)| { + let scalar = (i + 1) as u64; + insert_at(version_for(&party, scalar), &party, scalar, b) + }) + .collect(); let mut t_once = Tree::new(); - t_once.react( - bytes.iter().cloned().map(|b| insert_at(v.clone(), &party, 1, b))); + t_once.react(batch.clone()); let mut t_twice = Tree::new(); - t_twice.react( - bytes.iter().cloned().map(|b| insert_at(v.clone(), &party, 1, b))); - t_twice.react( - bytes.iter().cloned().map(|b| insert_at(v.clone(), &party, 1, b))); + t_twice.react(batch.clone()); + t_twice.react(batch); prop_assert_eq!(t_once, t_twice); } @@ -728,7 +742,7 @@ proptest! { // replay the event. This is the information a real synchronization // protocol would put on the wire. let mut tree_a: Tree = Tree::new(); - let mut a_events: Vec<(Key, Version, Message)> = Vec::new(); + let mut a_events: Vec<(Path, Version, Message)> = Vec::new(); for (i, value) in a_inserts.iter().enumerate() { let scalar = (i + 1) as u64; let mut recorded = tree_a.latest().clone(); @@ -738,7 +752,7 @@ proptest! { } let mut tree_b: Tree = Tree::new(); - let mut b_events: Vec<(Key, Version, Message)> = Vec::new(); + let mut b_events: Vec<(Path, Version, Message)> = Vec::new(); for (i, value) in b_inserts.iter().enumerate() { let scalar = (i + 1) as u64; let mut recorded = tree_b.latest().clone(); @@ -818,29 +832,32 @@ proptest! { tree.act(&party_of("P"), [insert_action(value.clone())]); tree.act(&party_of("P"), [insert_action(value.clone())]); - let path_v1 = leaf_path(&party, 1, &value); - let path_v2 = leaf_path(&party, 2, &value); + let path_v1 = leaf_path(&party, 1); + let path_v2 = leaf_path(&party, 2); prop_assert_ne!(path_v1, path_v2); - let got = [tree.get(&path_v1).unwrap(), tree.get(&path_v2).unwrap()]; + let got = [ + tree.get(&version_for(&party, 1)).unwrap(), + tree.get(&version_for(&party, 2)).unwrap(), + ]; prop_assert!(got.iter().all(|b| b.1[..] == *value)); } } -/// Forgetting a key the tree never held is a complete no-op: no leaf, and +/// Forgetting a path the tree never held is a complete no-op: no leaf, and /// — because the action was zero-effect — no version bump either, so the /// tree stays equal to a fresh one. #[test] fn delete_nonexistent_key() { let mut tree: Tree<()> = Tree::new(); - tree.act(&party_of("P"), [Action::Forget(Key([0; 32]))]); + tree.act(&party_of("P"), [Action::Forget(Path::from([0; 32]))]); assert_eq!(tree, Tree::new()); } -/// Project a borrowed leaf triple to an owned one, for collecting and +/// Project a borrowed leaf pair to an owned one, for collecting and /// comparing walk outputs. -fn owned((key, version, value): (Key, &Version, &Arc)) -> (Key, Version, Arc) { - (key, version.clone(), value.clone()) +fn owned((version, value): (&Version, &Arc)) -> (Version, Arc) { + (version.clone(), value.clone()) } proptest! { @@ -875,8 +892,8 @@ proptest! { tree.latest().clone(), other.latest().clone(), ]; - candidates.extend(tree.iter().map(|(_, v, _)| v.clone())); - candidates.extend(other.iter().map(|(_, v, _)| v.clone())); + candidates.extend(tree.iter().map(|(v, _)| v.clone())); + candidates.extend(other.iter().map(|(v, _)| v.clone())); let s = &candidates[start_sel.index(candidates.len())]; let e = &candidates[end_sel.index(candidates.len())]; @@ -889,21 +906,23 @@ proptest! { ) -> Result<(), TestCaseError> { let naive: Vec<_> = tree .iter() - .filter(|(_, version, _)| query.contains(version)) + .filter(|(version, _)| query.contains(version)) .map(owned) .collect(); let ranged: Vec<_> = tree.range(query).map(owned).collect(); prop_assert_eq!(&ranged, &naive, "range must equal the naive filter"); prop_assert!( - ranged.windows(2).all(|pair| pair[0].0 < pair[1].0), - "range yields ascending keys", + ranged + .windows(2) + .all(|pair| Path::for_leaf(&pair[0].0) < Path::for_leaf(&pair[1].0)), + "range yields ascending version-derived paths", ); let mut frozen = tree.range_owned(query); let mut thawed = Vec::new(); - while let Some((key, leaf)) = frozen.next() { - thawed.push((key, leaf.version().clone(), leaf.value().clone())); + while let Some((_, leaf)) = frozen.next() { + thawed.push((leaf.version().clone(), leaf.value().clone())); } prop_assert_eq!(&thawed, &naive, "the frozen walk must equal the naive filter"); Ok(()) @@ -936,8 +955,8 @@ proptest! { /// /// `iter`'s size hint equals the tree's length, the /// backward walk is the forward walk reversed, `get` finds every - /// iterated key with the same version and value, and a perturbed key - /// that names no leaf misses. + /// iterated version with the same version and value, and a version + /// that stamps no live leaf misses. #[test] fn iteration_and_point_lookup_agree( root in crate::tree::arb::arb_tree_root(0, 0..24), @@ -953,23 +972,21 @@ proptest! { backward.reverse(); prop_assert_eq!(&backward, &forward, "backward is forward reversed"); - for (key, version, value) in &forward { + for (version, value) in &forward { prop_assert_eq!( - tree.get(key), + tree.get(version), Some((version, value)), - "get resolves every iterated key", + "get resolves every iterated version", ); } if !forward.is_empty() { - let (key, ..) = &forward[flip.index(forward.len())]; - let mut bytes = *key.as_bytes(); - bytes[31] ^= 1; - let perturbed = Key::from(bytes); - // The flipped path could, in principle, name another live leaf; - // only assert the miss when it does not. - if !forward.iter().any(|(k, ..)| *k == perturbed) { - prop_assert_eq!(tree.get(&perturbed), None, "a foreign key misses"); + // A version one foreign tick past a live one stamps no leaf. + let (version, _) = &forward[flip.index(forward.len())]; + let mut perturbed = version.clone(); + perturbed.tick(&crate::tree::arb::nth_party(7)); + if !forward.iter().any(|(v, _)| *v == perturbed) { + prop_assert_eq!(tree.get(&perturbed), None, "a foreign version misses"); } } } @@ -1014,18 +1031,25 @@ proptest! { } for forget in forgets { - let keys: Vec = tree.iter().map(|(key, ..)| key).collect(); - let Some(&key) = keys.get(forget.index(keys.len().max(1))) else { + let versions: Vec = tree.iter().map(|(v, _)| v.clone()).collect(); + let Some(version) = versions.get(forget.index(versions.len().max(1))).cloned() + else { break; }; // Target the argmax half the time so the resize-down direction // is exercised on every run, not left to index luck. - let key = tree + let version = tree .iter() - .max_by_key(|(_, version, _)| version.as_bytes().len()) - .map(|(argmax, ..)| if forget.index(2) == 0 { argmax } else { key }) - .unwrap_or(key); - tree.act(&party_of("P"), [Action::Forget(key)]); + .max_by_key(|(version, _)| version.as_bytes().len()) + .map(|(argmax, _)| { + if forget.index(2) == 0 { + argmax.clone() + } else { + version.clone() + } + }) + .unwrap_or(version); + tree.act(&party_of("P"), [Action::Forget(Path::for_leaf(&version))]); prop_assert_eq!(tree.max_version_bytes(), naive_max_version_bytes(&tree)); } } @@ -1066,19 +1090,20 @@ proptest! { prop_assert_eq!(left.max_version_bytes(), naive_max_version_bytes(&left)); for forget in forgets { - let Some(key) = left + let Some(version) = left .iter() - .max_by_key(|(_, version, _)| version.as_bytes().len()) - .map(|(argmax, ..)| argmax) + .max_by_key(|(version, _)| version.as_bytes().len()) + .map(|(argmax, _)| argmax.clone()) .filter(|_| forget.index(2) == 0) .or_else(|| { - let keys: Vec = left.iter().map(|(key, ..)| key).collect(); - keys.get(forget.index(keys.len().max(1))).copied() + let versions: Vec = + left.iter().map(|(v, _)| v.clone()).collect(); + versions.get(forget.index(versions.len().max(1))).cloned() }) else { break; }; - left.act(&party_of("A"), [Action::Forget(key)]); + left.act(&party_of("A"), [Action::Forget(Path::for_leaf(&version))]); } left.join(right); @@ -1107,14 +1132,14 @@ proptest! { base_values in distinct_bytes(6), batch_values in distinct_bytes(4), forget_live in proptest::collection::vec(any::(), 0..4), - forget_missing in proptest::collection::vec(any::(), 0..3), + forget_missing in proptest::collection::vec(arb_path(), 0..3), ) { let mut tree: Tree = Tree::new(); tree.act( &party_of("A"), base_values.iter().cloned().map(insert_action), ); - let live: Vec = tree.iter().map(|(k, ..)| k).collect(); + let live: Vec = tree.iter().map(|(v, _)| Path::for_leaf(v)).collect(); let mut actions: Vec> = batch_values.iter().cloned().map(insert_action).collect(); @@ -1123,7 +1148,7 @@ proptest! { actions.push(Action::Forget(live[index.index(live.len())])); } } - // A drawn key matching a live one is astronomically unlikely but + // A drawn path matching a live one is astronomically unlikely but // harmless: the forget would then be effectual and both sides of // the equality move together. actions.extend(forget_missing.into_iter().map(Action::Forget)); @@ -1159,7 +1184,7 @@ proptest! { /// /// The pairs' paths share a drawn-length spine (the constructed /// analogue of a hash-prefix collision), driving the merge's divergent - /// arm at every level down to the split, where content-addressed pairs + /// arm at every level down to the split, where version-addressed pairs /// scatter at the root fan and never descend. Zero novelty widths are /// drawn too, so subset, identical, and ceiling-only merges — the /// flag's `false` arm — are sampled at depth alongside the gains. @@ -1234,12 +1259,12 @@ fn ceiling_only_join_reports_unchanged() { // ceiling. Its frontier is news to us; its (empty) content is not. let mut other: Tree = Tree::new(); other.act(&party_of("B"), [insert_action(Bytes::from_static(b"gone"))]); - let key = other + let version = other .iter() - .map(|(k, ..)| k) + .map(|(v, _)| v.clone()) .next() .expect("one live message"); - other.act(&party_of("B"), [Action::Forget(key)]); + other.act(&party_of("B"), [Action::Forget(Path::for_leaf(&version))]); assert!( other.is_empty(), "the counterparty redacted its only message" @@ -1275,9 +1300,8 @@ fn ceiling_only_join_reports_unchanged() { /// (pinned by `act_changed_flag_tracks_the_root_hash`). #[test] fn act_changed_flag_is_conservative_only_in_a_poisoned_store() { - let (receiver, poisoned, path, _escaped) = super::arb::uncontained_supply_pair(); + let (receiver, poisoned, key, escaped) = super::arb::uncontained_supply_pair(); let receiver_party = super::arb::nth_party(0); - let key = Key::from(path); let mut tree = Tree { root: receiver }; assert!( @@ -1297,7 +1321,7 @@ fn act_changed_flag_is_conservative_only_in_a_poisoned_store() { "the skipped forget left the root hash byte-identical", ); assert!( - tree.get(&key).is_some(), + tree.get(&escaped).is_some(), "the escaped leaf survives the skipped forget", ); } @@ -1315,15 +1339,17 @@ fn act_changed_flag_is_conservative_only_in_a_poisoned_store() { /// must happen at ingestion: once resident, the record is immortal. #[test] fn escaped_version_defeats_redaction_in_a_poisoned_store() { - let (receiver, poisoned, path, escaped) = super::arb::uncontained_supply_pair(); + let (receiver, poisoned, key, escaped) = super::arb::uncontained_supply_pair(); let receiver_party = super::arb::nth_party(0); - let key = Key::from(path); // Plant the escaped leaf by in-memory join: `Tree::join` is a local // merge, not wire ingestion, so no session tripwire guards it. let mut tree = Tree { root: receiver }; tree.join(Tree { root: poisoned }); - assert!(tree.get(&key).is_some(), "the join plants the escaped leaf"); + assert!( + tree.get(&escaped).is_some(), + "the join plants the escaped leaf" + ); assert!( !mirror::contained(&escaped, tree.latest()), "the merged ceiling never covers the escaped version", @@ -1333,7 +1359,7 @@ fn escaped_version_defeats_redaction_in_a_poisoned_store() { // ceiling, which the escaped version strictly dominates. tree.act(&receiver_party, [Action::Forget(key)]); assert!( - tree.get(&key).is_some(), + tree.get(&escaped).is_some(), "redacting the escaped leaf is silently skipped", ); @@ -1343,7 +1369,7 @@ fn escaped_version_defeats_redaction_in_a_poisoned_store() { let mut fresh: Tree<()> = Tree::new(); fresh.join(tree); assert!( - fresh.get(&key).is_some(), + fresh.get(&escaped).is_some(), "the escaped leaf re-plants into a fresh replica", ); } @@ -1519,7 +1545,7 @@ fn act_unwind_leaves_tree_byte_identical() { #[test] fn join_unwind_leaves_tree_byte_identical() { // Several divergent leaves per side spread the root fan across - // multiple radixes (paths are content hashes), so the root frame + // multiple radixes (paths are version hashes), so the root frame // performs several merge steps for the fuse to count. let mut ours: Tree = Tree::new(); ours.act( @@ -1569,7 +1595,7 @@ fn join_unwind_leaves_tree_byte_identical() { /// stay safe; only the drop is booby-trapped, and it holds fire while /// another panic is already unwinding (a second panic mid-unwind aborts /// the process instead of failing the test). -#[derive(Debug, borsh::BorshSerialize)] +#[derive(Debug, Serialize)] struct DropBomb { armed: bool, } @@ -1663,7 +1689,7 @@ fn act_mid_walk_unwind_leaves_tree_byte_identical() { fn act_destructor_unwind_leaves_tree_byte_identical() { let mut tree: Tree = Tree::new(); let existing = Message::new(DropBomb { armed: false }); - let key: Key = Path::for_leaf(&version_for("A", 2), existing.bytes()).into(); + let key = Path::for_leaf(&version_for("A", 2)); tree.react([(key, version_for("A", 2), existing)]); let hash_before = tree.hash(); @@ -1722,7 +1748,7 @@ fn join_destructor_unwind_leaves_tree_byte_identical() { // The key `act` derives for the bomb's insert (the second action on // this tree ticks party A to 2), computed up front so the redaction // below can name it. - let bomb_key: Key = Path::for_leaf(&version_for("A", 2), bomb.bytes()).into(); + let bomb_key = Path::for_leaf(&version_for("A", 2)); ours.act(&party_of("A"), [Action::Insert(bomb)]); // The counterparty forks while the bomb is live: the clone shares our @@ -1758,3 +1784,106 @@ fn join_destructor_unwind_leaves_tree_byte_identical() { "the ceiling is unchanged: no partial advance escapes the unwind" ); } + +/// Two leaves at one position digest equally whatever their contents — a +/// leaf digest is a pure function of its path suffix — so `Tree::join` +/// prunes the pair as equal, keeps its own side, and reports no change. +/// +/// The pair is planted directly through `react` at a synthetic shared +/// path, which no public insert can mint: this pins the digest's +/// suffix-only preimage behaviorally (the merge walk trusts path +/// derivation; collision detection is ingestion's job, where both leaves +/// are in hand). +#[test] +fn join_prunes_same_position_leaves_whatever_their_contents() { + let shared = Path::from([0x42; 32]); + + let mut ours: Tree = Tree::new(); + ours.react([(shared, version_for("A", 1), msg(Bytes::from_static(b"a")))]); + + let mut theirs: Tree = Tree::new(); + theirs.react([(shared, version_for("B", 1), msg(Bytes::from_static(b"b")))]); + + let hash_before = ours.hash(); + let changed = ours.join(theirs); + assert!(!changed, "a digest-equal pair teaches the set nothing"); + assert_eq!(ours.hash(), hash_before, "ours is kept verbatim"); + let (_, message) = ours + .iter() + .map(|(v, m)| (v.clone(), m.clone())) + .next() + .expect("one live message"); + assert_eq!(&*message, &Bytes::from_static(b"a"), "ours is kept"); +} + +/// Two leaves carrying the *same version* with different payloads compare +/// digest-equal, so `Tree::join` keeps one side and reports no change. +/// +/// Digests are content-blind by design: this is the modeled trade, pinned +/// so its boundary with ingestion's detected case (`react` asserting on a +/// disagreeing occupied-path insert) stays explicit. +#[test] +fn join_prunes_same_version_payload_divergence_as_equal() { + let version = version_for("A", 1); + let path = Path::for_leaf(&version); + + let mut ours: Tree = Tree::new(); + ours.react([(path, version.clone(), msg(Bytes::from_static(b"ours")))]); + let mut theirs: Tree = Tree::new(); + theirs.react([(path, version, msg(Bytes::from_static(b"theirs")))]); + + let hash_before = ours.hash(); + let changed = ours.join(theirs); + assert!(!changed, "a digest-equal pair teaches the set nothing"); + assert_eq!(ours.hash(), hash_before); + let (_, message) = ours + .iter() + .map(|(v, m)| (v.clone(), m.clone())) + .next() + .expect("one live message"); + assert_eq!(&*message, &Bytes::from_static(b"ours"), "ours is kept"); +} + +/// An insert landing on a live leaf that carries the same version and the +/// same payload bytes is the same send arriving twice: `react` keeps the +/// resident leaf and succeeds (idempotence), rather than erroring. +#[test] +fn reinserting_an_identical_leaf_is_idempotent() { + let version = version_for("A", 1); + let path = Path::for_leaf(&version); + let message = msg(Bytes::from_static(b"same")); + + let mut tree: Tree = Tree::new(); + tree.react([(path, version.clone(), message.clone())]); + let hash_before = tree.hash(); + tree.react([(path, version, message)]); + assert_eq!(tree.hash(), hash_before, "the tree is unchanged"); +} + +/// An insert landing on a live leaf that disagrees on payload bytes under +/// one version is version reuse: the apply walk asserts (a crate bug, +/// never an input — every production insert carries a freshly minted +/// version). +#[test] +#[should_panic(expected = "version reuse")] +fn react_asserts_on_version_reuse_at_an_occupied_path() { + let version = version_for("A", 1); + let path = Path::for_leaf(&version); + + let mut tree: Tree = Tree::new(); + tree.react([(path, version.clone(), msg(Bytes::from_static(b"first")))]); + tree.react([(path, version, msg(Bytes::from_static(b"second")))]); +} + +/// An insert landing on a live leaf whose version *differs* (a synthetic +/// path collision) trips the same assertion: both legs of the identity +/// check are enforced, not just payload equality. +#[test] +#[should_panic(expected = "version reuse")] +fn react_asserts_on_a_path_collision_between_distinct_versions() { + let shared = Path::from([0x24; 32]); + + let mut tree: Tree = Tree::new(); + tree.react([(shared, version_for("A", 1), msg(Bytes::from_static(b"a")))]); + tree.react([(shared, version_for("B", 1), msg(Bytes::from_static(b"a")))]); +} diff --git a/src/tree/traverse/act.rs b/src/tree/traverse/act.rs index 990db4562..bfa665770 100644 --- a/src/tree/traverse/act.rs +++ b/src/tree/traverse/act.rs @@ -23,6 +23,16 @@ pub enum Action { /// /// `actions` is consumed lazily: the only materialization is the radix sort /// at each branch level, so callers can feed a `map` chain straight in. +/// +/// # Panics +/// +/// Panics if an insert lands on a live leaf disagreeing with it on +/// version or payload: version reuse. No input reaches that state — +/// every production insert carries a freshly minted version (a fresh +/// tick strictly dominates the ceiling bounding every live leaf, and +/// party linearity keeps regions disjoint), and no wire-derived leaf +/// passes through this walk — so the panic marks a bug in this crate, +/// never an environmental failure. pub fn act( node: Option>, actions: I, @@ -34,7 +44,7 @@ where I: IntoIterator)>, { // Test-only unwind source for the panic-atomicity pins: this walk is - // the fallible region of `Tree::react`'s commit section, and its entry + // the unwind-source region of `Tree::react`'s commit section, and its entry // burns the first fuse step (each branch-level step below burns one // more). #[cfg(test)] @@ -170,6 +180,23 @@ impl Act for Z { continue; } + // Paths are version-derived, so an insert landing on a live + // leaf claims a version the tree already binds. Verify identity + // instead of assuming it: a byte-identical pair is the same + // send twice (keep the resident leaf), and any mismatch is + // version reuse — no input reaches it (fresh ticks strictly + // dominate the ceiling; party linearity keeps regions + // disjoint; no wire-derived leaf passes through this walk), so + // the assert marks a crate bug before anything commits. + if let (Action::Insert(value), Some(existing)) = (&action, &node) { + assert!( + *existing.ceiling() == version + && existing.message().as_slice() == value.as_slice(), + "version reuse: an insert landed on a live leaf disagreeing on version or payload", + ); + continue; + } + // Set the node node = match action { Action::Forget => None, diff --git a/src/tree/traverse/join.rs b/src/tree/traverse/join.rs index 9027867da..8b6fa74d5 100644 --- a/src/tree/traverse/join.rs +++ b/src/tree/traverse/join.rs @@ -18,13 +18,13 @@ //! subtree the other side learns; anything causally `<=` the other side's //! version was deleted there (the version vector is the entire deletion //! mechanism; there are no tombstones) and is dropped. -//! - **both have it, hashes equal**: the subtrees are identical (content -//! addressing makes equal hash ⟹ equal content, versions included), so keep -//! one verbatim. +//! - **both have it, hashes equal**: the subtrees hold the same version +//! set (paths are version-derived, so a hash commits the versions +//! beneath it), hence the same messages; keep one verbatim. //! - **both have it, hashes differ**: explode both one level and merge-walk //! the two ascending radix fans in lockstep, recursing only into the //! radixes whose child subtrees differ — children equal by pointer or by -//! content hash carry over verbatim through the shared structure — and +//! Merkle hash carry over verbatim through the shared structure — and //! reassembling with [`Node::branch`] (which re-compresses singletons and //! recomputes the joined branch version). //! @@ -51,7 +51,7 @@ use height::{Height, Root, S, Z}; /// by deletion honoring. The recursion decides this exactly, with no hashing: /// a gain is a subtree of `b` surviving the deletion filter where `a` held /// nothing, and a drop moves a node's exact memoized leaf count. Gains and -/// drops live at distinct content-addressed paths and each is monotone at its +/// drops live at distinct version-addressed paths and each is monotone at its /// path, so they cannot cancel: an untouched flag really means the merged /// tree is `a`, content-identical, equal root hash. pub fn join( @@ -65,7 +65,8 @@ where T: Send + Sync, { // Test-only unwind source for the panic-atomicity pin: the merge walk - // is the fallible region of `Tree::join`'s commit section, and its + // is the unwind-source region of `Tree::join`'s commit section (deletion + // honoring and the duplicate-subtree drops run `T` destructors), and its // entry burns the first fuse step (each branch-level step below burns // one more). #[cfg(test)] @@ -136,10 +137,9 @@ where } (Some(ours), Some(theirs)) => { // Identical subtrees: keep one. Equality short-circuits on - // shared backing (the common case for forked trees, hash-free) - // and otherwise on the content hash ⟹ equal content (content - // addressing). Either way there is nothing to learn on either - // side. + // shared backing (the common case for forked trees, + // hash-free), else on the Merkle hash — same version set, hence + // same messages: nothing to learn on either side. if ours == theirs { return Some(ours); } @@ -232,11 +232,15 @@ impl Join for Z { *changed |= gained.is_some(); gained } - // Two leaves at the same path are the same leaf: the path is the - // content-addressed hash of (version, value) (see - // `Path::for_leaf`), so identical paths carry identical contents. - // Keep one. - (Some(ours), Some(_)) => Some(ours), + // Two leaves at one position share the path, and a leaf digest + // is a pure function of its path (`Hash::leaf`), so the pair + // hashes equal and the level above carries it over verbatim + // without recursing. Collision detection is ingestion's job + // (`react`'s occupied-path arms), where both leaves are in + // hand; the merge walk trusts path derivation. + (Some(_), Some(_)) => { + unreachable!("same-position leaves hash equally and prune above") + } } } } diff --git a/src/tree/traverse/unknown/tests.rs b/src/tree/traverse/unknown/tests.rs index b4ea6e4d8..476d466bf 100644 --- a/src/tree/traverse/unknown/tests.rs +++ b/src/tree/traverse/unknown/tests.rs @@ -103,7 +103,7 @@ fn wide_divergence( for _ in 0..count { version.tick(&party); let message = Message::new(()); - let path = Path::for_leaf(&version, message.bytes()); + let path = Path::for_leaf(&version); actions.push((path, version.clone(), Action::Insert(message))); if flagged { known |= version.clone(); diff --git a/src/tree/typed/hash.rs b/src/tree/typed/hash.rs index 1be67ac73..3733a1187 100644 --- a/src/tree/typed/hash.rs +++ b/src/tree/typed/hash.rs @@ -1,26 +1,24 @@ use std::fmt::Debug; use std::sync::LazyLock; -use borsh::{BorshDeserialize, BorshSerialize}; - /// Width in bytes of the tree's Merkle hashes. /// /// The subtree-comparison digests that gossip exchanges, surfaced as /// [`Snapshot::hash`](crate::Snapshot::hash). Narrower than the 32-byte -/// [`Key`](crate::Key); the width argument is in [the reconciliation +/// version-derived leaf path; the width argument is in [the reconciliation /// docs](crate::reconciliation). pub const MERKLE_HASH_LEN: usize = 24; /// A 24-byte Merkle hash. /// -/// A newtype over a fixed-size byte array, so borsh can be derived without a -/// length prefix. +/// A newtype over a fixed-size byte array; on the wire it travels as its +/// raw bytes, never length-prefixed (the width is pinned by the type). /// /// The underlying primitive is [`blake3`], truncated to its leading /// [`MERKLE_HASH_LEN`] bytes — BLAKE3 is an extendable-output function, so /// prefix truncation is the sanctioned narrow form, with collision resistance /// 2⁹⁶ and preimage resistance 2¹⁹². Callers use [`Hash::of`] (or -/// [`ContentHash`] for the full width) and never touch the `blake3` types +/// [`PathHash`] for the full width) and never touch the `blake3` types /// directly. /// /// # Why 24 bytes here, and 32 for content @@ -45,9 +43,7 @@ pub const MERKLE_HASH_LEN: usize = 24; /// Hostile *peers* are off-model: peers in a universe trust one another /// ([the crate docs](crate) make a compromised member's powers explicit), /// so no width buys anything against a member, and none is priced here. -#[derive( - BorshSerialize, BorshDeserialize, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default, -)] +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default)] #[repr(transparent)] pub struct Hash(pub [u8; MERKLE_HASH_LEN]); @@ -59,9 +55,11 @@ impl Debug for Hash { /// Domain-separation tag leading a leaf's hash preimage. /// -/// Leaves are content-addressed (the path is the leaf's content hash; see -/// [`Path::for_leaf`](super::Path::for_leaf)), so a leaf's preimage commits -/// its compressed suffix — path bytes — and nothing else. +/// Leaves are version-addressed (the path is the full-width hash of the +/// leaf's version; see [`Path::for_leaf`](super::Path::for_leaf)), so a +/// leaf's preimage commits its compressed suffix — path bytes — and its +/// version's canonical encoding, never its message bytes: every compared +/// digest in the tree is a pure function of the version set. const LEAF_TAG: u8 = 0; /// Domain-separation tag leading a branch's hash preimage. @@ -82,7 +80,7 @@ impl Hash { /// One-shot Merkle hash of a contiguous byte slice: the leading /// [`MERKLE_HASH_LEN`] bytes of the full-width hash of the same bytes. pub fn of(bytes: &[u8]) -> Self { - ContentHash::of(bytes).truncate() + PathHash::of(bytes).truncate() } /// The hash of a leaf observed from the top of its compressed `suffix`: @@ -90,12 +88,18 @@ impl Hash { /// /// `suffix` is the leaf's path-compressed span in **path order** — /// shallowest byte first, as the node serializer emits it — and - /// `suffix_len` is one byte (a compressed span never exceeds the 32-byte - /// path). A leaf commits only its own path bytes: message and version - /// are already committed by *where* the leaf sits (leaves are - /// content-addressed; see [`Path::for_leaf`](super::Path::for_leaf)), - /// and each parent commits its child's radix byte, so a root-to-leaf - /// chain of preimages commits the full 32-byte path. + /// `suffix_len` is one byte (a compressed span never exceeds the + /// 32-byte path). + /// + /// The suffix is a complete commitment: a leaf's path is the + /// full-width hash of its version ([`Path::for_leaf`](super::Path::for_leaf)), + /// so under the crate's uniform-hash model one path is one version, + /// and every compared digest is a pure function of the version set. A + /// leaf commits no message bytes: a content author contributes no bit + /// to any compared quantity — digests are content-blind by design, a + /// modeled trade. Collision detection is ingestion's job + /// ([`react`](crate::tree::Tree::react)'s occupied-path arms), where + /// both leaves are in hand; the merge walk trusts path derivation. /// /// # Panics /// @@ -235,25 +239,23 @@ impl From for [u8; MERKLE_HASH_LEN] { } } -/// Full-width 32-byte BLAKE3 hash: the content-addressing primitive. +/// Full-width 32-byte BLAKE3 hash: the identity primitive a leaf's path +/// is made of. /// /// This is the width that carries identity. A leaf's path *is* a hash of this -/// width over its `(version, value)` (see -/// [`Path::for_leaf`](super::Path::for_leaf)), and -/// [`join`](crate::tree::traverse::join) resolves identical paths as identical -/// contents, so a collision here would be permanent, undetectable divergence — -/// full width is load-bearing, and every hash that feeds a path must use it (a -/// single Merkle-width component would cap the whole path's collision -/// resistance at the narrower width's). A `ContentHash` is never stored in a -/// branch and never -/// travels as a hash on the wire; it reaches the protocol only as a leaf's path -/// bytes. -pub struct ContentHash([u8; 32]); +/// width over its version's canonical bytes (see +/// [`Path::for_leaf`](super::Path::for_leaf)), and every ingestion site +/// treats one path as one identity, so a collision here would be permanent +/// split-brain — full width is load-bearing for the path even though the +/// comparison digests are narrower. A `PathHash` is never stored in a +/// branch and never travels as a hash on the wire; it reaches the protocol +/// only as a leaf's path bytes. +pub struct PathHash([u8; 32]); -impl ContentHash { +impl PathHash { /// One-shot full-width hash of a contiguous byte slice. pub fn of(bytes: &[u8]) -> Self { - ContentHash(*blake3::hash(bytes).as_bytes()) + PathHash(*blake3::hash(bytes).as_bytes()) } /// Truncate to the Merkle width: the leading [`MERKLE_HASH_LEN`] bytes. @@ -273,35 +275,11 @@ impl ContentHash { } } -impl From for [u8; 32] { - fn from(hash: ContentHash) -> Self { +impl From for [u8; 32] { + fn from(hash: PathHash) -> Self { hash.0 } } -/// Streaming full-width hasher: equivalent to feeding the concatenation of -/// every `update` chunk through [`ContentHash::of`], without allocating an -/// intermediate buffer. -#[derive(Default)] -pub struct Hasher(blake3::Hasher); - -impl Hasher { - /// Construct a fresh hasher. - pub fn new() -> Self { - Self::default() - } - - /// Append `bytes` to the hash input. - pub fn update(&mut self, bytes: &[u8]) -> &mut Self { - self.0.update(bytes); - self - } - - /// Finalize the hash and consume the hasher. - pub fn finalize(self) -> ContentHash { - ContentHash(*self.0.finalize().as_bytes()) - } -} - #[cfg(test)] mod tests; diff --git a/src/tree/typed/hash/tests.rs b/src/tree/typed/hash/tests.rs index 64f09c388..78a104dcd 100644 --- a/src/tree/typed/hash/tests.rs +++ b/src/tree/typed/hash/tests.rs @@ -2,7 +2,7 @@ //! the single-preimage node hash, and the collision pairs its kind tags and //! length fields exist to prevent. -use super::{BRANCH_TAG, ContentHash, Hash, LEAF_TAG, MERKLE_HASH_LEN}; +use super::{BRANCH_TAG, Hash, LEAF_TAG, MERKLE_HASH_LEN, PathHash}; /// A branch commits to exactly `BRANCH_TAG ‖ prefix_len ‖ prefix ‖ /// child_count ‖ (radix ‖ child_hash)*`. @@ -28,14 +28,16 @@ fn branch_preimage_layout() { } /// A leaf commits to exactly `LEAF_TAG ‖ suffix_len ‖ suffix` — its -/// compressed suffix, length-tagged, and nothing else. +/// compressed suffix, length-tagged, and never any version or message +/// bytes. +/// +/// The path (which the suffix spells) is version-derived, so the suffix +/// is already a complete commitment to the version set. #[test] fn leaf_preimage_layout() { let suffix = [0x01, 0x02, 0x03, 0x04]; - assert_eq!( - Hash::leaf(&suffix), - Hash::of(&[LEAF_TAG, 4, 0x01, 0x02, 0x03, 0x04]), - ); + let expected = vec![LEAF_TAG, 4, 0x01, 0x02, 0x03, 0x04]; + assert_eq!(Hash::leaf(&suffix), Hash::of(&expected)); } /// The empty tree hashes as a prefixless branch with no children — @@ -140,6 +142,6 @@ fn saturated_fan_count_uses_the_high_byte() { fn merkle_hash_is_prefix_of_full_width() { let preimage = b"any preimage at all"; let truncated = Hash::of(preimage); - let full = ContentHash::of(preimage); + let full = PathHash::of(preimage); assert_eq!(truncated.as_bytes()[..], full.as_bytes()[..MERKLE_HASH_LEN]); } diff --git a/src/tree/typed/height.rs b/src/tree/typed/height.rs index 3b044a00f..854bca832 100644 --- a/src/tree/typed/height.rs +++ b/src/tree/typed/height.rs @@ -123,7 +123,7 @@ impl_heights!( ); /// The height of the root: 32 levels above the leaves, one per byte of a -/// leaf's 32-byte content-addressed path. +/// leaf's 32-byte version-derived path. #[rustfmt::skip] pub type Root = // Laid out for your counting convenience in two rows of 16: diff --git a/src/tree/typed/node.rs b/src/tree/typed/node.rs index a1725f441..81e0eff3e 100644 --- a/src/tree/typed/node.rs +++ b/src/tree/typed/node.rs @@ -1,18 +1,22 @@ use std::{fmt::Debug, iter::Map, marker::PhantomData}; -use borsh::{BorshDeserialize, BorshSerialize}; - use before::{Dominance, Span}; use crate::{Version, causally, message::Message}; use super::hash::Hash; use super::height::{self, Height, S, Z}; + #[cfg(any(test, feature = "protocol-v1"))] use super::levels::{Top, levels}; use super::untyped; +#[cfg(any(test, feature = "protocol-v1"))] +use crate::tree::wire; use untyped::fan::{self, Fan}; +#[cfg(any(test, feature = "protocol-v1"))] +use serde::de::DeserializeOwned; + /// The typed node with a height of 32; the root of the tree. pub type Root = Node; @@ -382,11 +386,10 @@ impl Node { } /// Lazily iterate every live leaf in this root subtree as - /// `(Key, &Version, &Arc)`. + /// `([u8; 32], &Version, &Message)`. /// /// Delegates to the height-agnostic untyped walk; because this is a - /// height-32 root, every yielded path is a full 32-byte - /// [`Key`](crate::Key). + /// height-32 root, every yielded path is a full 32-byte array. pub fn iter(&self) -> untyped::Iter<'_, T> { untyped::Iter::root(&self.inner) } @@ -434,18 +437,19 @@ impl PartialEq for Node { } } -// Borsh wire format. Serialization is height-uniform: every typed -// `Node` delegates to [`untyped::Node::serialize_to`], which -// emits the in-memory representation directly (prefix length, head bytes, -// then either a leaf body or a `count_minus_two` + children list). No -// leaf-vs-branch tag is needed on the wire — at the receiver, the typed -// height together with the running `prefix_len` names the body's shape. +// Wire format (see [`crate::tree::wire`]). Serialization is +// height-uniform: every typed `Node` delegates to +// [`untyped::Node::serialize_to`], which emits the in-memory +// representation directly (prefix length, head bytes, then either a leaf +// body or a `count_minus_two` + children list). No leaf-vs-branch tag is +// needed on the wire — at the receiver, the typed height together with +// the running `prefix_len` names the body's shape. // // Deserialization at typed height `H` reads `prefix_len`, then either // decodes the body directly (when `prefix_len == 0`) or peels one head // byte and recurses at the next-finer typed height — synthesizing the // `prefix_len - 1` byte for the inner reader via -// [`borsh::io::Read::chain`]. The recursion bottoms out at the typed +// [`std::io::Read::chain`]. The recursion bottoms out at the typed // level matching the structural level of the underlying body: a multi- // child branch at `S<_>` heights, or a leaf at `Z`. // @@ -459,88 +463,74 @@ impl PartialEq for Node { // [`Node::branch`]. The wire's ascending radix order makes each insert an // appending binary-search miss, so the rebuild costs no shifting. -impl BorshSerialize for Node +#[cfg(any(test, feature = "protocol-v1"))] +impl wire::Encode for Node where H: Height, { - fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { self.inner.serialize_to(writer) } } -impl BorshDeserialize for Node +#[cfg(any(test, feature = "protocol-v1"))] +impl wire::Decode for Node where - T: BorshDeserialize, + T: DeserializeOwned, { - fn deserialize_reader(reader: &mut R) -> borsh::io::Result { - let prefix_len = u8::deserialize_reader(reader)?; + fn read_wire(reader: &mut R) -> std::io::Result { + let prefix_len = u8::read_wire(reader)?; if prefix_len != 0 { - return Err(borsh::io::Error::new( - borsh::io::ErrorKind::InvalidData, - "leaf height cannot carry a prefix", - )); + return Err(wire::invalid("leaf height cannot carry a prefix")); } - let version = Version::deserialize_reader(reader)?; - let message = Message::::deserialize_reader(reader)?; + let version = Version::read_wire(reader)?; + let message = Message::::read_wire(reader)?; Ok(Node::leaf(version, message)) } } -impl BorshDeserialize for Node> +#[cfg(any(test, feature = "protocol-v1"))] +impl wire::Decode for Node> where - T: BorshDeserialize, + T: DeserializeOwned, H: Height, S: Height, - Node: BorshDeserialize, + Node: wire::Decode, { - fn deserialize_reader(reader: &mut R) -> borsh::io::Result { - let prefix_len = u8::deserialize_reader(reader)?; + fn read_wire(reader: &mut R) -> std::io::Result { + let prefix_len = u8::read_wire(reader)?; if (prefix_len as usize) > >::HEIGHT { - return Err(borsh::io::Error::new( - borsh::io::ErrorKind::InvalidData, - "prefix length exceeds typed height", - )); + return Err(wire::invalid("prefix length exceeds typed height")); } if prefix_len == 0 { - let count_minus_two = u8::deserialize_reader(reader)?; + let count_minus_two = u8::read_wire(reader)?; let count = (count_minus_two as usize) + 2; if count > 256 { - return Err(borsh::io::Error::new( - borsh::io::ErrorKind::InvalidData, - "branch children count exceeds 256", - )); + return Err(wire::invalid("branch children count exceeds 256")); } let mut children = Children::::default(); let mut prev: Option = None; for _ in 0..count { - let radix = u8::deserialize_reader(reader)?; + let radix = u8::read_wire(reader)?; if let Some(p) = prev && radix <= p { - return Err(borsh::io::Error::new( - borsh::io::ErrorKind::InvalidData, - "branch radices not strictly ascending", - )); + return Err(wire::invalid("branch radices not strictly ascending")); } prev = Some(radix); - let child = Node::::deserialize_reader(reader)?; + let child = Node::::read_wire(reader)?; children.insert(radix, child); } - Node::branch(children).ok_or_else(|| { - borsh::io::Error::new( - borsh::io::ErrorKind::InvalidData, - "branch could not be reconstructed", - ) - }) + Node::branch(children).ok_or_else(|| wire::invalid("branch could not be reconstructed")) } else { - let head = u8::deserialize_reader(reader)?; + let head = u8::read_wire(reader)?; // Prepend `prefix_len - 1` to the rest of the stream so the // inner typed level reads it as if it were on the wire, // synthesizing the singleton-chain recursion without a helper // trait. let synthesized = [prefix_len - 1]; - let mut chained = borsh::io::Read::chain(synthesized.as_slice(), &mut *reader); - let inner = Node::::deserialize_reader(&mut chained)?; + let mut chained = std::io::Read::chain(synthesized.as_slice(), &mut *reader); + let inner = Node::::read_wire(&mut chained)?; Ok(Node::beneath(inner, head)) } } diff --git a/src/tree/typed/path.rs b/src/tree/typed/path.rs index f03fa7e62..551a8b99d 100644 --- a/src/tree/typed/path.rs +++ b/src/tree/typed/path.rs @@ -1,6 +1,6 @@ use std::{fmt::Debug, marker::PhantomData}; -use super::hash::{ContentHash, Hasher}; +use super::hash::PathHash; use super::height::{Height, Root, S}; use crate::Version; @@ -18,33 +18,24 @@ pub struct Path { } impl Path { - /// Get a path for the given leaf, incorporating its version and value. + /// Get the path for a leaf stamped with `version`: the full-width hash + /// of the version's canonical bytes, and nothing else. /// - /// The version's canonical [`as_bytes`](Version::as_bytes) makes the path - /// unique per insert: every [`tick`](Version::tick) yields a distinct - /// canonical encoding, so two content-identical values inserted at - /// different versions land at distinct paths. Parties descend from a - /// shared seed by disjoint forks, so their versions are structurally - /// distinct too; the version alone therefore disambiguates without also - /// folding in the party. - pub fn for_leaf(version: &Version, value: &[u8]) -> Self { - // We form the hash for a value as the binary depth-1 merkle tree of - // version, value. This ensures no length malleability issues. - // - // Every component is the full-width `ContentHash`, never the - // truncated Merkle `Hash`: the path's collision resistance is the - // minimum over its component hashes, and a path collision is - // permanent split-brain (see `ContentHash`). A Merkle-width inner - // hash here would cap the whole path at the narrower width's - // strength despite its 32-byte output. - - let mut hasher = Hasher::new(); - hasher.update(ContentHash::of(version.as_bytes()).as_bytes()); - hasher.update(ContentHash::of(value).as_bytes()); - + /// Versions are unique per send — locally by [`tick`](Version::tick) + /// (each tick changes the canonical [`as_bytes`](Version::as_bytes)), + /// globally by party disjointness — an invariant the protocol already + /// rests on everywhere, so version-derived identity adds no assumption. + /// Message bytes enter no path and no digest: no actor can steer where + /// anything lands by choosing content. + /// + /// The path is the full-width 32-byte `PathHash`, never the + /// truncated Merkle `Hash`: a path collision is permanent split-brain + /// (see `PathHash`). The preimage is one self-delimiting canonical + /// byte string, so no concatenation ambiguity arises. + pub fn for_leaf(version: &Version) -> Self { Self { height: PhantomData, - hash: hasher.finalize().into(), + hash: PathHash::of(version.as_bytes()).into(), } } } diff --git a/src/tree/typed/path/tests.rs b/src/tree/typed/path/tests.rs index ab8f00403..9f9f27d28 100644 --- a/src/tree/typed/path/tests.rs +++ b/src/tree/typed/path/tests.rs @@ -1,4 +1,3 @@ -use bytes::Bytes; use proptest::prelude::*; use crate::tree::arb::arb_version; @@ -6,27 +5,18 @@ use crate::tree::arb::arb_version; use super::*; proptest! { - /// `for_leaf` commits to its `(version, value)` through *full-width* - /// component hashes: the path is `blake3(blake3(version) ‖ - /// blake3(value))`, 32 bytes wide at every stage. + /// `for_leaf` is exactly the *full-width* hash of the version's + /// canonical bytes: `blake3(version)`, 32 bytes, no other input. /// - /// Full width at every component is what keeps a path collision at 2^128 - /// birthday strength; a truncated Merkle-width hash anywhere in the - /// construction would cap the whole path below that, and this pin fails - /// under that wrong reading. + /// Full width is what keeps a path collision at 2^128 birthday + /// strength (a truncated Merkle-width hash would cap it lower), and + /// the version's canonical bytes are the whole preimage: no message + /// byte can steer where a leaf lands. This pin fails under either + /// wrong reading. #[test] - fn for_leaf_components_are_full_width( - version in arb_version(), - value in any::>(), - ) { - let value = Bytes::from(value); - let expected: [u8; 32] = { - let mut buf = Vec::with_capacity(64); - buf.extend_from_slice(blake3::hash(version.as_bytes()).as_bytes()); - buf.extend_from_slice(blake3::hash(value.as_ref()).as_bytes()); - *blake3::hash(&buf).as_bytes() - }; - let path = Path::for_leaf(&version, &value); + fn for_leaf_is_the_full_width_version_hash(version in arb_version()) { + let expected: [u8; 32] = *blake3::hash(version.as_bytes()).as_bytes(); + let path = Path::for_leaf(&version); prop_assert_eq!(<[u8; 32]>::from(path), expected); } diff --git a/src/tree/typed/prefix.rs b/src/tree/typed/prefix.rs index 36fe74c9a..ab7911925 100644 --- a/src/tree/typed/prefix.rs +++ b/src/tree/typed/prefix.rs @@ -1,9 +1,9 @@ use std::{fmt::Debug, marker::PhantomData}; -use borsh::{BorshDeserialize, BorshSerialize}; use tinyvec::ArrayVec; -use crate::tree::Key; +#[cfg(any(test, feature = "protocol-v1"))] +use crate::tree::wire; use super::height::{Height, Root, S, Z}; use super::path::Path; @@ -39,17 +39,17 @@ impl From for Path { } } -impl From for Key { +impl From for [u8; 32] { fn from(value: Prefix) -> Self { Path::from(value).into() } } -impl From for Prefix { - fn from(value: Key) -> Self { +impl From<[u8; 32]> for Prefix { + fn from(value: [u8; 32]) -> Self { Self { height: PhantomData, - hash: <[u8; 32]>::from(value).into(), + hash: value.into(), } } } @@ -80,7 +80,7 @@ where impl Prefix { /// The accumulated path bytes, shallowest-first. Exactly `32 - H::HEIGHT` /// long, so appending the remaining `H::HEIGHT` bytes of a descent below - /// this point reconstructs a full 32-byte [`Key`]. + /// this point reconstructs a full 32-byte path. pub fn as_bytes(&self) -> &[u8] { &self.hash } @@ -158,8 +158,9 @@ impl Debug for Prefix { /// On the wire a `Prefix` is exactly `32 - H::HEIGHT` raw bytes. The height /// is pinned by the type, so no length prefix is transmitted: deserialization /// reads exactly the byte count the type demands. -impl BorshSerialize for Prefix { - fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { +#[cfg(any(test, feature = "protocol-v1"))] +impl wire::Encode for Prefix { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { let expected = 32 - H::HEIGHT; debug_assert_eq!( self.hash.len(), @@ -172,8 +173,9 @@ impl BorshSerialize for Prefix { } } -impl BorshDeserialize for Prefix { - fn deserialize_reader(reader: &mut R) -> borsh::io::Result { +#[cfg(any(test, feature = "protocol-v1"))] +impl wire::Decode for Prefix { + fn read_wire(reader: &mut R) -> std::io::Result { let len = 32 - H::HEIGHT; let mut hash: ArrayVec<[u8; 32]> = ArrayVec::new(); // Reserve `len` zero slots so we can read directly into the buffer. diff --git a/src/tree/typed/tests.rs b/src/tree/typed/tests.rs index a56e8cadb..7f014a454 100644 --- a/src/tree/typed/tests.rs +++ b/src/tree/typed/tests.rs @@ -1,20 +1,20 @@ -use borsh::BorshDeserialize; use proptest::prelude::*; use crate::tree::typed::height::{Height, Root, S, Z}; use crate::tree::typed::{Hash, Prefix, hash::MERKLE_HASH_LEN}; +use crate::tree::wire; proptest! { - /// A `Hash` borsh round-trips losslessly as exactly its + /// A `Hash` wire round-trips losslessly as exactly its /// `MERKLE_HASH_LEN` raw bytes. /// The trivial fixed-width case, pinned so a future encoding change to - /// the helper trait surfaces here first. + /// the wire codec surfaces here first. #[test] - fn hash_borsh_round_trip(bytes in any::<[u8; MERKLE_HASH_LEN]>()) { + fn hash_wire_round_trip(bytes in any::<[u8; MERKLE_HASH_LEN]>()) { let original = Hash(bytes); - let serialized = borsh::to_vec(&original).unwrap(); + let serialized = wire::to_vec(&original).unwrap(); prop_assert_eq!(serialized.len(), MERKLE_HASH_LEN); - let deserialized = Hash::try_from_slice(&serialized).unwrap(); + let deserialized: Hash = wire::from_slice(&serialized).unwrap(); prop_assert_eq!(original, deserialized); } } @@ -26,7 +26,7 @@ fn prefix_from_bytes(bytes: &[u8]) -> Prefix { let expected_len = 32 - H::HEIGHT; assert_eq!(bytes.len(), expected_len); let serialized = bytes.to_vec(); - Prefix::::try_from_slice(&serialized).expect("known-valid prefix bytes") + wire::from_slice(&serialized).expect("known-valid prefix bytes") } /// `Prefix` is encoded as exactly `32 - H::HEIGHT` raw bytes with no @@ -35,23 +35,23 @@ fn prefix_from_bytes(bytes: &[u8]) -> Prefix { macro_rules! prefix_roundtrip_test { ($name:ident, $height:ty) => { proptest! { - /// The prefix's fixed-width Borsh form round-trips exactly at this height. + /// The prefix's fixed-width wire form round-trips exactly at this height. #[test] fn $name(bytes in proptest::collection::vec(any::(), 32 - <$height>::HEIGHT)) { let prefix: Prefix<$height> = prefix_from_bytes(&bytes); - let serialized = borsh::to_vec(&prefix).unwrap(); + let serialized = wire::to_vec(&prefix).unwrap(); prop_assert_eq!(serialized.len(), 32 - <$height>::HEIGHT); prop_assert_eq!(serialized.as_slice(), bytes.as_slice()); - let deserialized = Prefix::<$height>::try_from_slice(&serialized).unwrap(); + let deserialized: Prefix<$height> = wire::from_slice(&serialized).unwrap(); prop_assert_eq!(prefix, deserialized); } } }; } -prefix_roundtrip_test!(prefix_borsh_round_trip_z, Z); -prefix_roundtrip_test!(prefix_borsh_round_trip_s_z, S); -prefix_roundtrip_test!(prefix_borsh_round_trip_root, Root); +prefix_roundtrip_test!(prefix_wire_round_trip_z, Z); +prefix_roundtrip_test!(prefix_wire_round_trip_s_z, S); +prefix_roundtrip_test!(prefix_wire_round_trip_root, Root); /// A `Prefix` is exactly zero bytes on the wire (the root has no /// prefix). Pin the empty serialization so a future change to the encoding @@ -59,6 +59,6 @@ prefix_roundtrip_test!(prefix_borsh_round_trip_root, Root); #[test] fn prefix_root_serializes_to_empty() { let prefix = Prefix::::new(); - let serialized = borsh::to_vec(&prefix).unwrap(); + let serialized = wire::to_vec(&prefix).unwrap(); assert!(serialized.is_empty()); } diff --git a/src/tree/typed/untyped.rs b/src/tree/typed/untyped.rs index 1fd2be264..1d7b62ed6 100644 --- a/src/tree/typed/untyped.rs +++ b/src/tree/typed/untyped.rs @@ -2,7 +2,6 @@ use std::fmt::Debug; use std::mem; use std::sync::{Arc, OnceLock}; -use borsh::BorshSerialize; use tinyvec::ArrayVec; use before::{Dominance, Span}; @@ -445,8 +444,8 @@ impl Node { /// /// Forked trees share their unchanged subtrees by `Arc`, so an in-memory /// merge can short-circuit those in `O(1)`, even with cold memos, before - /// falling back to the content hash for subtrees that diverged in memory - /// but hold equal content. + /// falling back to the Merkle hash for subtrees that diverged in memory + /// but hold the same version set. pub fn ptr_eq(&self, other: &Self) -> bool { Arc::ptr_eq(&self.inner, &other.inner) } @@ -652,9 +651,10 @@ impl Node { self.inner.prefix.len() } - /// Borsh-serialize the node in its in-memory layout. + /// Serialize the node in its in-memory layout. /// - /// This is the canonical encoder: the typed `BorshSerialize` impl is a + /// This is the canonical encoder: the typed + /// [`wire::Encode`](crate::tree::wire::Encode) impl is a /// thin delegate over it, and on the decode side the same shape is /// reconstructed via the chain-reader trick that synthesizes per-level /// `prefix_len` bytes. @@ -665,7 +665,8 @@ impl Node { /// 2. `prefix_len` head bytes, shallowest first (decoders peel from the /// outermost compressed level inward); /// 3. the body, dispatched on `children`: - /// - [`Children::Leaf`]: `version: Version`, then `message: Message`; + /// - [`Children::Leaf`]: `version`, then `message`, each one CBOR + /// value (self-delimiting; see [`wire`](crate::tree::wire)); /// - [`Children::Branch`]: `count_minus_two: u8`, then for each /// child (in ascending radix order, structural in the fan): /// `radix: u8`, `serialize_to(child)`. @@ -674,23 +675,21 @@ impl Node { /// typed height and the running `prefix_len` together name the body's /// shape. Multi-child branches always carry at least two children, by /// the path-compression invariant. - pub fn serialize_to(&self, writer: &mut W) -> borsh::io::Result<()> { - let prefix_len = u8::try_from(self.inner.prefix.len()).map_err(|_| { - borsh::io::Error::new( - borsh::io::ErrorKind::InvalidData, - "node prefix length does not fit in a u8", - ) - })?; - prefix_len.serialize(writer)?; + #[cfg(any(test, feature = "protocol-v1"))] + pub fn serialize_to(&self, writer: &mut W) -> std::io::Result<()> { + use crate::tree::wire::{Encode, invalid}; + let prefix_len = u8::try_from(self.inner.prefix.len()) + .map_err(|_| invalid("node prefix length does not fit in a u8"))?; + prefix_len.write_wire(writer)?; // Wire order is shallowest-first; the in-memory `prefix` stores the // shallowest byte at the last index, so iterate in reverse. for byte in self.inner.prefix.iter().rev() { - byte.serialize(writer)?; + byte.write_wire(writer)?; } match &self.inner.children { Children::Leaf { message, version } => { - version.serialize(writer)?; - message.serialize(writer)?; + version.write_wire(writer)?; + message.write_wire(writer)?; } Children::Branch { children, .. } => { debug_assert!( @@ -698,14 +697,11 @@ impl Node { "multi-child branch must have 2..=256 children", ); let count_minus_two = u8::try_from(children.len() - 2).map_err(|_| { - borsh::io::Error::new( - borsh::io::ErrorKind::InvalidData, - "branch children count does not fit in count_minus_two: u8", - ) + invalid("branch children count does not fit in count_minus_two: u8") })?; - count_minus_two.serialize(writer)?; + count_minus_two.write_wire(writer)?; for (radix, child) in children.iter() { - radix.serialize(writer)?; + radix.write_wire(writer)?; child.serialize_to(writer)?; } } @@ -747,8 +743,13 @@ impl Eq for Node {} impl PartialEq for Node { fn eq(&self, other: &Self) -> bool { // Shared backing settles equality with no hashing (and even cold): the - // common case for forked/cloned trees and the subtrees they share. Only - // distinct allocations fall back to the content hash. + // common case for forked/cloned trees and the subtrees they share. + // Distinct allocations fall back to the Merkle hash, which commits + // shape and version set — never message bytes — so this is + // version-set equality — and content equality, because no two + // messages ever share a version. (Same-version leaves with + // different payloads would compare equal; producing such a pair + // takes an already-fatal linearity violation.) self.ptr_eq(other) || self.hash() == other.hash() } } diff --git a/src/tree/typed/untyped/iter.rs b/src/tree/typed/untyped/iter.rs index 4a05dce69..35e0e40c3 100644 --- a/src/tree/typed/untyped/iter.rs +++ b/src/tree/typed/untyped/iter.rs @@ -10,6 +10,7 @@ use std::collections::VecDeque; use tinyvec::ArrayVec; use crate::causally::{Coverage, Polarity, Query}; + use crate::{Version, causally, message::Message}; use super::{Children, Node}; @@ -18,10 +19,6 @@ use super::{Children, Node}; struct Frame<'a, T> { /// The subtree not yet entered. node: &'a Node, - /// The path bytes accumulated to reach `node` (above its own compressed - /// prefix), inline: the tree's depth is fixed at 32, so a leaf's full - /// path always fits and the buffer never spills to the heap. - path: ArrayVec<[u8; 32]>, /// Whether an ancestor was already promoted: every leaf beneath `node` /// is known to satisfy the walk's range, so its descent skips the /// version comparisons. @@ -33,18 +30,16 @@ struct Frame<'a, T> { /// [`Query`]. /// /// [`Iter`] passes [`causally::all`], whose one root classification -/// promotes the whole walk. The walk yields each leaf's reconstructed -/// 32-byte path [`Key`], its [`Version`], and a borrowed handle to its -/// [`Message`]. +/// promotes the whole walk. The walk yields each leaf's [`Version`] and a +/// borrowed handle to its [`Message`]; a leaf's location is a pure +/// function of its version, so no path is reconstructed (the owned walk, +/// [`RangeOwned`], is the one that yields paths — its consumers key +/// leaves by them). /// /// The walk is lazy: a single step descends only far enough to reach the /// next leaf, so the first item is produced after walking one root-to-leaf -/// spine rather than the whole tree. Each pending node in the frontier -/// carries the path bytes accumulated to reach it (above its own compressed -/// prefix); since the tree's depth is fixed at 32, those fit an inline -/// [`ArrayVec<[u8; 32]>`](ArrayVec) (the same shape as -/// [`Prefix`](crate::tree::typed::Prefix)), so the only allocation the walk -/// ever makes is the frontier deque itself. +/// spine rather than the whole tree; the only allocation the walk ever +/// makes is the frontier deque itself. /// /// A popped subtree is classified before it is entered: one /// [`coverage`](Query::coverage) verdict over its memoized @@ -55,7 +50,6 @@ struct Frame<'a, T> { /// its verdict degenerates to membership and prune-or-promote is /// exhaustive: the walk never compares versions leaf-by-leaf. /// -/// [`Key`]: crate::tree::key::Key struct Walk<'a, T, P: Polarity> { /// Pending [`Frame`]s, held in ascending key order front-to-back. /// @@ -80,26 +74,21 @@ struct Walk<'a, T, P: Polarity> { } impl<'a, T, P: Polarity> Walk<'a, T, P> { - fn new(node: Option<&'a Node>, path: &[u8], query: Query<'a, P>) -> Self { + fn new(node: Option<&'a Node>, query: Query<'a, P>) -> Self { match node { None => Self { frames: VecDeque::new(), remaining: 0, query, }, - Some(node) => { - let mut buf = ArrayVec::new(); - buf.extend_from_slice(path); - Self { - frames: VecDeque::from([Frame { - node, - path: buf, - passes: false, - }]), - remaining: node.len(), - query, - } - } + Some(node) => Self { + frames: VecDeque::from([Frame { + node, + passes: false, + }]), + remaining: node.len(), + query, + }, } } @@ -111,12 +100,8 @@ impl<'a, T, P: Polarity> Walk<'a, T, P> { /// ordered so the frontier stays ascending front-to-back; the two ends /// therefore never yield the same leaf and meet cleanly when the frontier /// empties. - fn step(&mut self, back: bool) -> Option<(crate::tree::key::Key, &'a Version, &'a Message)> { - 'frontier: while let Some(Frame { - node, - mut path, - passes, - }) = if back { + fn step(&mut self, back: bool) -> Option<(&'a Version, &'a Message)> { + 'frontier: while let Some(Frame { node, passes }) = if back { self.frames.pop_back() } else { self.frames.pop_front() @@ -132,50 +117,31 @@ impl<'a, T, P: Polarity> Walk<'a, T, P> { Coverage::Full => true, Coverage::Partial => false, }; - // The compressed prefix sits above this node's level and is stored - // shallowest-last, so replay it shallowest-first to extend the path. - for &byte in node.inner.prefix.iter().rev() { - path.push(byte); - } match &node.inner.children { Children::Leaf { message, .. } => { // A leaf's span is coincident, so its coverage verdict is // never Partial: reaching here means it passes. debug_assert!(passes, "an unpruned leaf passes its query"); - debug_assert_eq!( - path.len(), - 32, - "a leaf sits at depth 32, so its path is 32 bytes" - ); - let path = path.into_inner(); self.remaining -= 1; - return Some((crate::tree::key::Key(path), node.ceiling(), message)); + return Some((node.ceiling(), message)); } Children::Branch { children, .. } => { - // Re-push the children onto the end we just popped, each - // with its own extended copy of the inline path buffer - // (the per-frame buffer is what keeps the descent lazy). - // Order so the frontier stays ascending front-to-back: + // Re-push the children onto the end we just popped, + // ordered so the frontier stays ascending front-to-back: // pushing to the front goes largest-radix-first so the // smallest ends up frontmost; pushing to the back goes // smallest-radix-first so the largest ends up backmost. if back { - for (radix, child) in children.iter() { - let mut child_path = path; - child_path.push(radix); + for (_, child) in children.iter() { self.frames.push_back(Frame { node: child, - path: child_path, passes, }); } } else { - for (radix, child) in children.iter().rev() { - let mut child_path = path; - child_path.push(radix); + for (_, child) in children.iter().rev() { self.frames.push_front(Frame { node: child, - path: child_path, passes, }); } @@ -188,8 +154,7 @@ impl<'a, T, P: Polarity> Walk<'a, T, P> { } /// A lazy depth-first iterator over every live leaf in a subtree, yielding -/// each leaf's reconstructed 32-byte path [`Key`], its [`Version`], and a -/// borrowed handle to its [`Message`]. +/// each leaf's [`Version`] and a borrowed handle to its [`Message`]. /// /// For the same walk filtered to a causal range, see [`Range`]. /// @@ -197,54 +162,41 @@ impl<'a, T, P: Polarity> Walk<'a, T, P> { /// serialization alongside the `Arc`); callers that only want the value /// project it cheaply with [`Message::as_arc`]. /// -/// [`next`](Iterator::next) yields leaves in ascending-key order; the iterator -/// is also a [`DoubleEndedIterator`], so [`next_back`](DoubleEndedIterator::next_back) -/// yields them in descending-key order, and the two ends meet in the middle -/// without overlap. Keys are content-derived hashes, so key order bears *no* -/// relation to the causal order on [`Version`]s: a leaf may be yielded -/// before one that causally precedes it. (The public observers on -/// [`Rumors`](crate::Rumors) still promise nothing about order, but -/// [`unknown`](crate::tree::traverse::unknown) and `Tree::join` lean on the -/// ascending forward order for their own deterministic callback delivery.) -/// -/// `Iter` is `Send + Sync` whenever `T: Send + Sync`: it holds only `&Node` -/// references and inline path buffers. +/// [`next`](Iterator::next) yields leaves in ascending order of their +/// version-derived paths; the iterator is also a [`DoubleEndedIterator`], +/// so [`next_back`](DoubleEndedIterator::next_back) yields them in +/// descending path order, and the two ends meet in the middle without +/// overlap. Path order bears *no* relation to the causal order on +/// [`Version`]s: a leaf may be yielded before one that causally precedes +/// it. (The public observers on [`Rumors`](crate::Rumors) still promise +/// nothing about order, but [`unknown`](crate::tree::traverse::unknown) +/// and `Tree::join` lean on the ascending forward order for their own +/// deterministic callback delivery.) /// -/// [`Key`]: crate::tree::key::Key +/// `Iter` is `Send + Sync` whenever `T: Send + Sync`: it holds only +/// `&Node` references. pub struct Iter<'a, T> { walk: Walk<'a, T, causally::Neutral>, } impl<'a, T> Iter<'a, T> { - /// Iterate the subtree rooted at `node` (a height-32 root, so every leaf's - /// path is a full 32-byte [`Key`](crate::tree::key::Key)). + /// Iterate the subtree rooted at `node`. pub(crate) fn root(node: &'a Node) -> Self { - Self::within(node, &[]) - } - - /// Iterate the subtree rooted at `node` when it does *not* sit at the top - /// of the tree. - /// - /// `path` carries the bytes already walked to reach it (the ancestors' - /// radixes, shallowest-first), which the descent extends so each leaf - /// still reconstructs a full 32-byte [`Key`](crate::tree::key::Key). - /// `path.len()` plus the height of `node` must therefore be 32. - pub(crate) fn within(node: &'a Node, path: &[u8]) -> Self { Self { - walk: Walk::new(Some(node), path, causally::all()), + walk: Walk::new(Some(node), causally::all()), } } /// The empty iterator, for a tree with no root. pub(crate) fn empty() -> Self { Self { - walk: Walk::new(None, &[], causally::all()), + walk: Walk::new(None, causally::all()), } } } impl<'a, T> Iterator for Iter<'a, T> { - type Item = (crate::tree::key::Key, &'a Version, &'a Message); + type Item = (&'a Version, &'a Message); fn next(&mut self) -> Option { self.walk.step(false) @@ -290,13 +242,13 @@ impl<'a, T, P: Polarity> Range<'a, T, P> { /// whose versions the causal `query` admits. pub(crate) fn root(node: Option<&'a Node>, query: Query<'a, P>) -> Self { Self { - walk: Walk::new(node, &[], query), + walk: Walk::new(node, query), } } } impl<'a, T, P: Polarity> Iterator for Range<'a, T, P> { - type Item = (crate::tree::key::Key, &'a Version, &'a Message); + type Item = (&'a Version, &'a Message); fn next(&mut self) -> Option { self.walk.step(false) @@ -334,7 +286,7 @@ impl<'a, T, P: Polarity> DoubleEndedIterator for Range<'a, T, P> { /// borrowing walk (see [`Range`]); forward-only, since its consumers are /// subscription drains. /// Yields each passing leaf as an owned [`Leaf`] handle alongside its -/// reconstructed [`Key`](crate::tree::key::Key), which is what lets a caller +/// reconstructed 32-byte path, which is what lets a caller /// lend `&Version` / `&Arc` out of a leaf it keeps. pub struct RangeOwned { /// The not-yet-visited root, consumed by the first advance. @@ -417,7 +369,7 @@ impl RangeOwned { /// `path` carries the bytes already walked to reach `node` (the /// ancestors' radixes, shallowest-first), which the descent extends so /// each leaf still reconstructs a full 32-byte - /// [`Key`](crate::tree::key::Key). `path.len()` plus the height of + /// 32-byte path. `path.len()` plus the height of /// `node` must therefore be 32. pub(crate) fn within(node: Option>, path: &[u8], query: Query<'static, P>) -> Self { let mut buf = ArrayVec::new(); @@ -435,7 +387,7 @@ impl RangeOwned { /// Advance to the next passing leaf. The same classification as the /// borrowing walk, with the leaf handed out by value. - pub(crate) fn next(&mut self) -> Option<(crate::tree::key::Key, Leaf)> { + pub(crate) fn next(&mut self) -> Option<([u8; 32], Leaf)> { loop { // Obtain the next unvisited node — the initial root, or the next // child at the deepest spine level, ascending past exhausted @@ -516,7 +468,7 @@ impl RangeOwned { 32, "a leaf sits at depth 32, so its path is 32 bytes" ); - let key = crate::tree::key::Key(self.path.into_inner()); + let key = self.path.into_inner(); self.path.truncate(rollback); return Some((key, Leaf(node))); } diff --git a/src/tree/typed/untyped/tests.rs b/src/tree/typed/untyped/tests.rs index 2e2cb44d0..0afdc8571 100644 --- a/src/tree/typed/untyped/tests.rs +++ b/src/tree/typed/untyped/tests.rs @@ -595,7 +595,7 @@ fn node_hash_preimage_is_in_path_order() { const LEAF_TAG: u8 = 0; let leaf = Node::leaf(Version::new(), Message::new(())); let wrapped = leaf.beneath(0xAA).beneath(0xBB); - assert_eq!(wrapped.hash(), super::Hash::of(&[LEAF_TAG, 2, 0xBB, 0xAA]),); + assert_eq!(wrapped.hash(), super::Hash::of(&[LEAF_TAG, 2, 0xBB, 0xAA])); } /// A hand-built two-leaf tree pins the preimages end to end. diff --git a/src/tree/wire.rs b/src/tree/wire.rs new file mode 100644 index 000000000..4a420690f --- /dev/null +++ b/src/tree/wire.rs @@ -0,0 +1,269 @@ +//! The alternating protocol's byte codec: explicit structural framing over +//! `std::io`, with every variable-width atom one CBOR value. +//! +//! Lives beside the tree because the node serializer is one of its +//! encoders; the streaming protocol has its own codec and uses only the +//! CBOR atoms. +//! +//! Container shapes (counts, radix records, fixed-width prefixes and +//! hashes) are protocol framing, written and validated by hand exactly +//! like the streaming codec's signal and length headers. The two atoms a +//! frame cannot delimit itself — a [`Version`] and a [`Message`] — ride +//! as single CBOR values, self-delimiting by CBOR's own length headers: +//! the version as a byte string wrapping its canonical encoding (the +//! `before` serde form), the message as a byte string wrapping its cached +//! CBOR payload. Decoding pulls exactly one value off the stream, so the +//! bytes after an atom belong to the next field. + +use std::io::{Read, Write}; + +use crate::Version; +use crate::message::Message; +use crate::tree::typed::Hash; + +use serde::de::DeserializeOwned; +/// Encode `self` onto a byte stream. +/// +/// The method is `write_wire`, not `encode_to`: `before`'s types carry +/// inherent `encode_to` methods (the bare canonical codec), and an +/// inherent method silently shadows a trait method at call sites — a +/// shadowed call here would write unframed bytes where the decoder +/// expects a CBOR value. +pub(crate) trait Encode { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()>; +} + +/// Decode one `Self` off a byte stream, consuming exactly its bytes. +/// +/// Named `read_wire` for the same shadowing reason as +/// [`Encode::write_wire`]. +pub(crate) trait Decode: Sized { + fn read_wire(reader: &mut R) -> std::io::Result; +} + +/// Encode a value into a fresh buffer. +pub(crate) fn to_vec(value: &T) -> std::io::Result> { + let mut buf = Vec::new(); + value.write_wire(&mut buf)?; + Ok(buf) +} + +/// Decode a value from an exact slice, rejecting trailing bytes. +pub(crate) fn from_slice(mut bytes: &[u8]) -> std::io::Result { + let value = T::read_wire(&mut bytes)?; + if !bytes.is_empty() { + return Err(invalid(format!( + "{} trailing bytes after the decoded value", + bytes.len() + ))); + } + Ok(value) +} + +/// An `InvalidData` error with the given message. +pub(crate) fn invalid(message: impl Into) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::InvalidData, message.into()) +} + +/// Map a ciborium serialization failure into the stream's error type. +/// +/// Value errors are unreachable for the types this codec writes (their +/// `Serialize` impls emit exactly one byte string), so every failure here +/// is the writer's. +fn ser_error(error: ciborium::ser::Error) -> std::io::Error { + match error { + ciborium::ser::Error::Io(error) => error, + ciborium::ser::Error::Value(message) => invalid(message), + } +} + +/// Map a ciborium deserialization failure into the stream's error type, +/// preserving the truncation/corruption split the callers classify by. +fn de_error(error: ciborium::de::Error) -> std::io::Error { + match error { + ciborium::de::Error::Io(error) => error, + error => invalid(error.to_string()), + } +} + +impl Encode for u8 { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + writer.write_all(&[*self]) + } +} + +impl Decode for u8 { + fn read_wire(reader: &mut R) -> std::io::Result { + let mut byte = [0u8; 1]; + reader.read_exact(&mut byte)?; + Ok(byte[0]) + } +} + +/// A container's length: a little-endian `u32` count. +impl Encode for u32 { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + writer.write_all(&self.to_le_bytes()) + } +} + +impl Decode for u32 { + fn read_wire(reader: &mut R) -> std::io::Result { + let mut bytes = [0u8; 4]; + reader.read_exact(&mut bytes)?; + Ok(u32::from_le_bytes(bytes)) + } +} + +/// A length-counted sequence: `u32` LE count, then each element. +impl Encode for Vec { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + let count = u32::try_from(self.len()) + .map_err(|_| invalid("container length does not fit in a u32 count"))?; + count.write_wire(writer)?; + for item in self { + item.write_wire(writer)?; + } + Ok(()) + } +} + +impl Decode for Vec { + fn read_wire(reader: &mut R) -> std::io::Result { + let count = u32::read_wire(reader)? as usize; + // Grow as elements arrive rather than trusting the declared count + // for the allocation (the same discipline as the framing reader). + let mut items = Vec::new(); + for _ in 0..count { + items.push(A::read_wire(reader)?); + } + Ok(items) + } +} + +/// An optional value: a presence byte (0 or 1), then the value. +impl Encode for Option { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + match self { + None => 0u8.write_wire(writer), + Some(value) => { + 1u8.write_wire(writer)?; + value.write_wire(writer) + } + } + } +} + +impl Decode for Option { + fn read_wire(reader: &mut R) -> std::io::Result { + match u8::read_wire(reader)? { + 0 => Ok(None), + 1 => Ok(Some(A::read_wire(reader)?)), + tag => Err(invalid(format!("invalid Option tag byte {tag}"))), + } + } +} + +/// A pair: the two fields back to back. +impl Encode for (A, B) { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + self.0.write_wire(writer)?; + self.1.write_wire(writer) + } +} + +impl Decode for (A, B) { + fn read_wire(reader: &mut R) -> std::io::Result { + Ok((A::read_wire(reader)?, B::read_wire(reader)?)) + } +} + +/// A Merkle hash: its raw fixed-width bytes (the width is pinned by the +/// type, so no length travels). +impl Encode for Hash { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + writer.write_all(self.as_bytes()) + } +} + +impl Decode for Hash { + fn read_wire(reader: &mut R) -> std::io::Result { + let mut bytes = [0u8; crate::tree::typed::hash::MERKLE_HASH_LEN]; + reader.read_exact(&mut bytes)?; + Ok(Hash(bytes)) + } +} + +/// One CBOR value: a byte string wrapping the canonical version encoding. +impl Encode for Version { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + ciborium::ser::into_writer(self, writer).map_err(ser_error) + } +} + +impl Decode for Version { + fn read_wire(reader: &mut R) -> std::io::Result { + ciborium::de::from_reader(reader).map_err(de_error) + } +} + +/// One CBOR value: a byte string wrapping the cached CBOR payload. +impl Encode for Message { + fn write_wire(&self, writer: &mut W) -> std::io::Result<()> { + ciborium::ser::into_writer(self, writer).map_err(ser_error) + } +} + +impl Decode for Message { + fn read_wire(reader: &mut R) -> std::io::Result { + ciborium::de::from_reader(reader).map_err(de_error) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::message::Message; + use crate::tree::arb::nth_party; + use crate::tree::typed::height::Z; + use crate::tree::typed::{Node, Prefix}; + + /// Every wire atom round-trips through `to_vec`/`from_slice`, alone + /// and composed: the version and message as CBOR values, the node's + /// structural framing around them, and a `Vec` of prefix-node pairs. + /// + /// The leaf case is the shadowing tripwire: `Version::encode_to` (the + /// bare canonical codec) must never be what the node serializer calls, + /// or the version travels unframed and the decode misaligns. + #[test] + fn atoms_round_trip() { + let party = nth_party(0); + let mut version = crate::Version::new(); + version.tick(&party); + + let v = to_vec(&version).unwrap(); + let back: crate::Version = from_slice(&v).unwrap(); + assert_eq!(back, version); + + let m = Message::new(()); + let enc = to_vec(&m).unwrap(); + let back: Message<()> = from_slice(&enc).unwrap(); + assert_eq!(back.as_slice(), m.as_slice()); + + let leaf: Node<(), Z> = Node::leaf(version.clone(), Message::new(())); + let enc = to_vec(&leaf).unwrap(); + let back: Node<(), Z> = from_slice(&enc).unwrap(); + assert_eq!(back.hash(), leaf.hash()); + + let pair: Vec<(Prefix, Node<(), Z>)> = vec![( + Prefix::from(<[u8; 32]>::from(crate::tree::typed::Path::for_leaf( + &version, + ))), + leaf.clone(), + )]; + let enc = to_vec(&pair).unwrap(); + let back: Vec<(Prefix, Node<(), Z>)> = from_slice(&enc).unwrap(); + assert_eq!(back.len(), 1); + assert_eq!(back[0].1.hash(), leaf.hash()); + } +} diff --git a/src/tutorial.rs b/src/tutorial.rs index 71e28fb77..2aee639ba 100644 --- a/src/tutorial.rs +++ b/src/tutorial.rs @@ -96,7 +96,7 @@ //! //! alice.send("the meeting is at noon".to_string()); //! -//! for (_key, _version, message) in alice.snapshot().iter() { +//! for (_version, message) in alice.snapshot().iter() { //! println!("alice holds: {message}"); //! } //! Ok(()) @@ -109,9 +109,9 @@ //! //! A bare `send` statement commits right there, as the statement ends; //! chaining several changes into one commit is -//! [`Batch`](crate::Batch)'s job. Notice that the snapshot yields a key -//! and a version alongside each message — we ignore them for now, and the -//! key returns in step 6. +//! [`Batch`](crate::Batch)'s job. Notice that the snapshot yields a +//! [`Version`](crate::Version) alongside each message — the message's +//! identity, which we ignore for now; it returns in step 6. //! //! # Step 4: bootstrap Bob //! @@ -145,7 +145,7 @@ //! .into_rumors(); //! server.await.expect("alice's serving task"); //! -//! for (_key, _version, message) in bob.snapshot().iter() { +//! for (_version, message) in bob.snapshot().iter() { //! println!("bob holds: {message}"); //! } //! Ok(()) @@ -225,11 +225,12 @@ //! //! # Step 6: redact, and watch it vanish //! -//! The meeting is over; take the message back. Redaction needs the -//! message's [`Key`](crate::Key), and keys come back out of observation — -//! snapshots and the [message streams](crate#how-should-you-observe-messages) -//! attach one to every message — so we find the key by looking, then hand -//! it to [`redact`](crate::Rumors::redact) and let the drivers spread the +//! The meeting is over; take the message back. Redaction names a message +//! by its [`Version`](crate::Version), and versions come back out of +//! observation — snapshots and the [message +//! streams](crate#how-should-you-observe-messages) attach one to every +//! message — so we find the version by looking, then hand it to +//! [`redact`](crate::Rumors::redact) and let the drivers spread the //! deletion. With this final addition, the whole program reads: //! //! ``` @@ -265,13 +266,14 @@ //! assert_eq!(bob.snapshot().len(), 2); //! println!("bob holds {} messages", bob.snapshot().len()); //! -//! // New: find the key by observing, redact, and drive one more session. +//! // New: find the version by observing, redact, and drive one more +//! // session. //! let snapshot = alice.snapshot(); -//! let (key, _version, _message) = snapshot +//! let (version, _message) = snapshot //! .iter() -//! .find(|(_, _, message)| message.as_str() == "the meeting is at noon") +//! .find(|(_, message)| message.as_str() == "the meeting is at noon") //! .expect("alice still holds the meeting message"); -//! alice.redact(key); +//! alice.redact(version); //! //! let (pushed, served) = tokio::join!(alice_drive.next(), bob_drive.next()); //! pushed.expect("alice's driver is running")?; @@ -279,7 +281,7 @@ //! //! assert_eq!(alice.snapshot().len(), 1); //! assert_eq!(bob.snapshot().len(), 1); -//! for (_key, _version, message) in bob.snapshot().iter() { +//! for (_version, message) in bob.snapshot().iter() { //! println!("bob still holds: {message}"); //! } //! Ok(()) diff --git a/tests/async_wire.rs b/tests/async_wire.rs index 05bdee297..f2d4e1020 100644 --- a/tests/async_wire.rs +++ b/tests/async_wire.rs @@ -6,13 +6,13 @@ //! //! Wire gossip *is* the merge — there is no in-process join to compare //! against — so the oracle is the abstract union of the two pre-session -//! readouts: sound because the peers tick disjoint parties, never share -//! keys, and only ever redact keys they themselves minted before the -//! session. +//! readouts: sound because the peers tick disjoint parties, never mint +//! the same version, and only ever redact messages they themselves minted +//! before the session. //! //! Both tests share the `Insert`/`Redact` action shape, so redactions cross //! the wire too (not just inserts), and run against both a primitive (`u64`) -//! and a non-primitive (`String`) value type to cover the borsh round-trip. +//! and a non-primitive (`String`) value type to cover the payload round-trip. mod common; @@ -59,7 +59,7 @@ proptest! { } /// String-T variant of [`async_gossip_converges_on_the_union`]: same - /// invariant for `T = String`, exercising the borsh round-trip for a + /// invariant for `T = String`, exercising the payload round-trip for a /// non-primitive value type over the concurrent wire. #[test] fn async_gossip_converges_on_the_union_string( diff --git a/tests/bookmark_causality.rs b/tests/bookmark_causality.rs index f36191e2c..036155ebc 100644 --- a/tests/bookmark_causality.rs +++ b/tests/bookmark_causality.rs @@ -63,7 +63,7 @@ use std::sync::{Arc, Mutex}; use before::Party; use proptest::prelude::*; -use rumors::{Error, Key, MERKLE_HASH_LEN, Network, Peer, Retire, Rumors, Version}; +use rumors::{Error, MERKLE_HASH_LEN, Network, Peer, Retire, Rumors, Version}; use crate::common::fault::{self, FaultPlan}; use crate::common::flaky::{DurableStore, FaultFeed, FlakyInMemoryBookmark, persisted_record}; @@ -207,7 +207,7 @@ fn store_covers(record: &BTreeMap>, emission: &Emission) - } /// Decode the id-regions a node has durably checkpointed for `network`, via the -/// same Borsh round trip the bookmark itself makes. +/// same encode/decode round trip the bookmark itself makes. /// /// The dual of /// [`decompose_store`]: that keeps each clock's version (for durability), this @@ -394,7 +394,7 @@ impl World { // race-free and the just-sent unique id is present exactly once. let snapshot = rumors.snapshot(); let mut version = None; - for (_key, leaf_version, value) in snapshot.iter() { + for (leaf_version, value) in snapshot.iter() { if **value == id { version = Some(leaf_version.clone()); break; @@ -416,11 +416,11 @@ impl World { return; }; let snapshot = rumors.snapshot(); - let keys: Vec = snapshot.iter().map(|(key, _, _)| key).collect(); - if keys.is_empty() { + let versions: Vec = snapshot.iter().map(|(v, _)| v.clone()).collect(); + if versions.is_empty() { return; } - rumors.redact(keys[which % keys.len()]); + rumors.redact(&versions[which % versions.len()]); } /// Promote every pending emission of `who` that has become **known to the @@ -870,7 +870,7 @@ impl World { let rumors = self.nodes[k].live().unwrap(); let network = rumors.network(); let snapshot = rumors.snapshot(); - for (_key, leaf_version, value) in snapshot.iter() { + for (leaf_version, value) in snapshot.iter() { live_leaves += 1; let seq = **value; assert!( diff --git a/tests/bookmark_transmit_window.rs b/tests/bookmark_transmit_window.rs index 8b9b793a8..e7b59f397 100644 --- a/tests/bookmark_transmit_window.rs +++ b/tests/bookmark_transmit_window.rs @@ -193,8 +193,8 @@ fn leaf_version(rumors: &Rumors, payload: Msg) -> Option { - // Redacting a key the application currently holds always records - // a deletion in the subject's own region, ticking it; redacting - // nothing (an empty set) is a true no-op. Liveness is read from - // the snapshot — the application's own view — never from the - // version arithmetic the suppression uses. - let keys: Vec = self + // Redacting a message the application currently holds always + // records a deletion in the subject's own region, ticking it; + // redacting nothing (an empty set) is a true no-op. Liveness + // is read from the snapshot — the application's own view — + // never from the version arithmetic the suppression uses. + let versions: Vec = self .probe .subject .snapshot() .iter() - .map(|(k, _, _)| k) + .map(|(v, _)| v.clone()) .collect(); - if !keys.is_empty() { - self.probe.subject.redact(keys[i % keys.len()]); + if !versions.is_empty() { + self.probe.subject.redact(&versions[i % versions.len()]); self.model.local_change(); } None diff --git a/tests/bootstrap.rs b/tests/bootstrap.rs index e5e4cde73..5757456db 100644 --- a/tests/bootstrap.rs +++ b/tests/bootstrap.rs @@ -21,6 +21,8 @@ use crate::common::flaky::{DurableStore, FaultFeed, FlakyInMemoryBookmark, persi use crate::common::oracle::readout; use crate::common::wire::{assert_control_drained, block_on, bootstrap_fork, wire_gossip}; +use serde::Serialize; +use serde::de::DeserializeOwned; /// Capacity for each in-memory link stream. Roomy enough that the bootstrap /// descent's largest frames fit without the test depending on backpressure /// subtleties. @@ -30,7 +32,7 @@ const LINK_BUF: usize = 64 * 1024; /// link, returning whatever the bootstrapper produced. fn wire_bootstrap(provider: &Rumors) -> Option> where - T: borsh::BorshSerialize + borsh::BorshDeserialize + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { block_on(async move { let (mut a_link, mut b_link) = rumors::link::memory_with_capacity(LINK_BUF); @@ -50,8 +52,9 @@ where proptest! { /// Bootstrapping from a provider yields exactly the provider's live - /// `(Key, value)` content (keys are stable across peers), leaves the - /// provider's own content untouched, and mints a *disjoint* party. + /// content, message identities included (versions are stable across + /// peers), leaves the provider's own content untouched, and mints a + /// *disjoint* party. /// /// Disjointness is proven behaviorally: a message the newcomer originates /// survives a gossip round back into the provider, which a non-disjoint or @@ -81,13 +84,13 @@ proptest! { bootstrapped.send(u64::MAX); wire_gossip(&provider, &bootstrapped); prop_assert!( - provider.snapshot().iter().any(|(_, _, m)| **m == u64::MAX), + provider.snapshot().iter().any(|(_, m)| **m == u64::MAX), "the newcomer's origination must survive gossip into the provider", ); } /// `String`-`T` variant of [`bootstrap_reproduces_a_fork`]: the same - /// invariant for a non-primitive value type, exercising the borsh + /// invariant for a non-primitive value type, exercising the wire /// round-trip of the whole-tree frame for `T = String`. #[test] fn bootstrap_reproduces_a_fork_string(actions in arb_string_actions()) { @@ -111,7 +114,7 @@ proptest! { bootstrapped.send("newcomer's own".to_string()); wire_gossip(&provider, &bootstrapped); prop_assert!( - provider.snapshot().iter().any(|(_, _, m)| **m == "newcomer's own"), + provider.snapshot().iter().any(|(_, m)| **m == "newcomer's own"), "the newcomer's origination must survive gossip into the provider", ); } @@ -189,7 +192,7 @@ fn zero_budget_bootstrap_converges() { bootstrapped.send(u64::MAX); wire_gossip(&provider, &bootstrapped); assert!( - provider.snapshot().iter().any(|(_, _, m)| **m == u64::MAX), + provider.snapshot().iter().any(|(_, m)| **m == u64::MAX), "the newcomer's origination must survive its zero-budget gossip", ); } diff --git a/tests/bootstrap_snapshot.rs b/tests/bootstrap_snapshot.rs index 78106aeb7..1e9b93c89 100644 --- a/tests/bootstrap_snapshot.rs +++ b/tests/bootstrap_snapshot.rs @@ -23,7 +23,6 @@ mod common; -use borsh::{BorshDeserialize, BorshSerialize}; use rand::SeedableRng; use rand::rngs::SmallRng; #[cfg(feature = "protocol-v1")] @@ -34,6 +33,8 @@ use crate::common::gossip_snapshot::capture_session; #[cfg(feature = "protocol-v1")] use crate::common::gossip_snapshot::capture_session_v1; +use serde::Serialize; +use serde::de::DeserializeOwned; /// A provider seeded from a fixed RNG, so the [`rumors::Network`] id carried in /// the preamble — and the party region it forks off for the newcomer — are /// deterministic and these captures stay reproducible. @@ -50,7 +51,7 @@ fn seeded() -> Rumors { /// expected to be served a successor. fn capture_bootstrap(provider: Rumors) -> String where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { capture_session( move |mut link| async move { @@ -119,10 +120,10 @@ fn v1_populated_provider() { /// Bootstrap of a non-primitive, variable-length payload. /// -/// `u64` borsh-encodes -/// to a fixed 8 bytes; `String` encodes as a length prefix followed by its -/// UTF-8 bytes, so this is the only bootstrap scenario that pins how a -/// variable-length value is framed inside a served leaf. +/// A `u64` payload CBOR-encodes +/// as one compact integer; a `String` encodes as a CBOR text string with +/// its own length header, so this is the only bootstrap scenario that pins +/// how a variable-length value is framed inside a served leaf. #[test] fn string_payload() { let provider: Rumors = seeded(); diff --git a/tests/causal.rs b/tests/causal.rs index 59c0435ab..c7741c691 100644 --- a/tests/causal.rs +++ b/tests/causal.rs @@ -18,16 +18,16 @@ use std::collections::{BTreeMap, BTreeSet}; use futures::FutureExt; use proptest::collection::vec; use proptest::prelude::*; -use rumors::{CausalMessages, Key, Peer, Rumors, Version}; +use rumors::{CausalMessages, Peer, Rumors, Version}; -use crate::common::action::minted_key; +use crate::common::action::minted_version; use crate::common::wire::{bootstrap_fork, wire_gossip}; /// One observer step, with the borrowed faces cloned out. #[derive(Debug, PartialEq)] enum Step { /// The observer yielded a message. - Item((Key, Version, u64)), + Item((Version, u64)), /// The observer is quiet: nothing new, actors still live. Quiet, /// The observer ended: every sender is gone and the complete final @@ -40,13 +40,13 @@ fn step(obs: &mut CausalMessages) -> Step { match obs.borrow_next().now_or_never() { None => Step::Quiet, Some(None) => Step::Ended, - Some(Some((k, v, m))) => Step::Item((k, v.clone(), **m)), + Some(Some((v, m))) => Step::Item((v.clone(), **m)), } } /// Drain the observer until it goes quiet or ends, returning the items in /// delivery order and whether it ended. -fn drain(obs: &mut CausalMessages) -> (Vec<(Key, Version, u64)>, bool) { +fn drain(obs: &mut CausalMessages) -> (Vec<(Version, u64)>, bool) { let mut items = Vec::new(); loop { match step(obs) { @@ -63,11 +63,11 @@ fn drain(obs: &mut CausalMessages) -> (Vec<(Key, Version, u64)>, bool) { // `Version` is a partial order: `!(later < earlier)` also admits concurrent // pairs, which `later >= earlier` would reject. #[allow(clippy::neg_cmp_op_on_partial_ord)] -fn assert_causal(items: &[(Key, Version, u64)]) { +fn assert_causal(items: &[(Version, u64)]) { for i in 0..items.len() { for j in (i + 1)..items.len() { assert!( - !(items[j].1 < items[i].1), + !(items[j].0 < items[i].0), "causal inversion: item {j} ({:?}) causally precedes item {i} ({:?})", items[j].0, items[i].0, @@ -76,25 +76,32 @@ fn assert_causal(items: &[(Key, Version, u64)]) { } } -/// The live `Key → value` map, for comparing against deliveries. -fn live_map(rumors: &Rumors) -> BTreeMap { - rumors.snapshot().iter().map(|(k, _, m)| (k, **m)).collect() +/// The live identity → value map, keyed by canonical version bytes, for +/// comparing against deliveries. +fn live_map(rumors: &Rumors) -> BTreeMap, u64> { + rumors + .snapshot() + .iter() + .map(|(v, m)| (v.as_bytes().to_vec(), **m)) + .collect() } /// Drain an [`rumors::UnorderedMessages`] observer until it goes quiet or -/// ends, returning the delivered `(Key, value)` pairs. -fn drain_unordered(obs: &mut rumors::UnorderedMessages) -> Vec<(Key, u64)> { +/// ends, returning the delivered `(Version, value)` pairs. +fn drain_unordered(obs: &mut rumors::UnorderedMessages) -> Vec<(Version, u64)> { let mut items = Vec::new(); loop { match obs.try_next() { - rumors::TryNext::Message((key, _, message)) => items.push((key, **message)), + rumors::TryNext::Message((version, message)) => { + items.push((version.clone(), **message)) + } rumors::TryNext::Quiet | rumors::TryNext::Ended => return items, } } } /// A single party's sends form a causal chain, so a fresh observer must -/// deliver the whole backlog in exactly send order — the case key-ordered +/// deliver the whole backlog in exactly send order — the case path-ordered /// delivery scrambles roughly half the time. #[test] fn single_party_backlog_replays_in_send_order() { @@ -107,7 +114,7 @@ fn single_party_backlog_replays_in_send_order() { let (items, ended) = drain(&mut obs); assert!(!ended, "the set is live: quiet, not ended"); assert_eq!( - items.iter().map(|(_, _, m)| *m).collect::>(), + items.iter().map(|(_, m)| *m).collect::>(), (0..8).collect::>(), "a causal chain is delivered in chain order" ); @@ -116,7 +123,7 @@ fn single_party_backlog_replays_in_send_order() { /// A backlog mixing one party's chain with a concurrent peer's (learned via /// gossip) is delivered without causal inversions, and concurrent messages -/// come out in the deterministic `(rank, key)` order. +/// come out in the deterministic (rank, canonical version bytes) order. #[test] fn converged_backlog_has_no_inversions() { let a = Peer::::seed().sync_window_floor().into_rumors(); @@ -135,12 +142,16 @@ fn converged_backlog_has_no_inversions() { assert_eq!(items.len(), 8, "both chains are in the converged backlog"); assert_causal(&items); - // One ingest batch pops in (rank, key) order: the delivered sequence is - // sorted by causal rank, which is what makes it deterministic. - let ranks: Vec<_> = items.iter().map(|(k, v, _)| (v.rank(), *k)).collect(); + // One ingest batch pops in (rank, canonical version bytes) order: the + // delivered sequence is sorted by causal rank — what makes it + // deterministic — with the canonical encoding breaking rank ties. + let ranks: Vec<_> = items + .iter() + .map(|(v, _)| (v.rank(), v.as_bytes().to_vec())) + .collect(); assert!( ranks.windows(2).all(|w| w[0] < w[1]), - "a single backlog drains in strictly increasing (rank, key) order" + "a single backlog drains in strictly increasing (rank, bytes) order" ); } @@ -256,33 +267,37 @@ fn staged_then_redacted_is_still_delivered() { let known = Peer::::seed().sync_window_floor().into_rumors(); let pre = known.snapshot().latest().clone(); known.send(1); - let key_1 = minted_key(&known.snapshot(), &pre); + let version_1 = minted_version(&known.snapshot(), &pre); let pre = known.snapshot().latest().clone(); known.send(2); - let key_2 = minted_key(&known.snapshot(), &pre); + let version_2 = minted_version(&known.snapshot(), &pre); // First step ingests the whole pass (both messages) and delivers the // causally least; the other is staged. let mut obs = known.causal_messages(); - let Step::Item((delivered_key, ..)) = step(&mut obs) else { + let Step::Item((delivered_version, _)) = step(&mut obs) else { panic!("a populated set delivers an item"); }; - let staged_key = if delivered_key == key_1 { key_2 } else { key_1 }; + let staged_version = if delivered_version == version_1 { + version_2 + } else { + version_1 + }; // Redact the staged message, then drain: it is delivered anyway (it was // live at its ingest), and nothing fires after. - known.redact(staged_key); + known.redact(&staged_version); let (items, _) = drain(&mut obs); assert_eq!( - items.iter().map(|(k, _, _)| *k).collect::>(), - vec![staged_key], + items.iter().map(|(v, _)| v.clone()).collect::>(), + vec![staged_version], "the staged message outlives its redaction by exactly one delivery" ); // Redacted wholly before any ingest: never delivered. let pre = known.snapshot().latest().clone(); known.send(3); - known.redact(minted_key(&known.snapshot(), &pre)); + known.redact(&minted_version(&known.snapshot(), &pre)); let (items, _) = drain(&mut obs); assert!(items.is_empty(), "pre-ingest redactions never fire"); } @@ -305,7 +320,7 @@ fn observer_drains_the_final_state_causally_then_ends() { assert_eq!( items .iter() - .map(|(k, _, m)| (*k, *m)) + .map(|(v, m)| (v.as_bytes().to_vec(), *m)) .collect::>(), expected, "the complete final state is yielded before the end" @@ -326,11 +341,11 @@ fn stream_face_is_causal_and_terminates() { let mut obs = known.causal_messages(); let mut items = Vec::new(); - while let Some(Some((k, v, m))) = obs.next().now_or_never() { - items.push((k, v, *m)); + while let Some(Some((v, m))) = obs.next().now_or_never() { + items.push((v, *m)); } assert_eq!( - items.iter().map(|(_, _, m)| *m).collect::>(), + items.iter().map(|(_, m)| *m).collect::>(), (0..6).collect::>(), "the Stream face replays the chain in order" ); @@ -352,8 +367,8 @@ enum Op { SendA(u64), /// `b` sends this value (concurrent to `a` until a gossip). SendB(u64), - /// Redact the `idx % minted`-th key minted at `a` so far (dropped if - /// none). + /// Redact the `idx % minted`-th message minted at `a` so far (dropped + /// if none). Redact(usize), /// Converge the replicas. Gossip, @@ -378,8 +393,8 @@ proptest! { /// The whole contract under arbitrary interleaving of local sends, /// concurrent peer sends, redactions, gossip, and partial drains. /// - /// The cumulative delivered sequence has no causal inversion, no key fires - /// twice, and the deliveries cover the final live set. + /// The cumulative delivered sequence has no causal inversion, no message + /// fires twice, and the deliveries cover the final live set. /// /// Causal order costs nothing in coverage relative to the plain observer. #[test] @@ -388,22 +403,22 @@ proptest! { let b = bootstrap_fork(&a); let mut obs = a.causal_messages(); - let mut minted: Vec = Vec::new(); - let mut delivered: Vec<(Key, Version, u64)> = Vec::new(); + let mut minted: Vec = Vec::new(); + let mut delivered: Vec<(Version, u64)> = Vec::new(); for op in &ops { match op { Op::SendA(v) => { let pre = a.snapshot().latest().clone(); a.send(*v); - minted.push(minted_key(&a.snapshot(), &pre)); + minted.push(minted_version(&a.snapshot(), &pre)); } Op::SendB(v) => { b.send(*v); } Op::Redact(idx) => { if !minted.is_empty() { - a.redact(minted[idx % minted.len()]); + a.redact(&minted[idx % minted.len()]); } } Op::Gossip => wire_gossip(&a, &b), @@ -424,12 +439,17 @@ proptest! { // Exactly-once and coverage, as the plain observer promises. let mut seen = BTreeSet::new(); - for (key, _, _) in &delivered { - prop_assert!(seen.insert(*key), "key {key:?} delivered twice"); + for (version, _) in &delivered { + prop_assert!( + seen.insert(version.as_bytes().to_vec()), + "version {version:?} delivered twice" + ); } for (key, value) in &final_live { prop_assert!( - delivered.iter().any(|(k, _, m)| k == key && m == value), + delivered + .iter() + .any(|(v, m)| v.as_bytes() == key.as_slice() && m == value), "a final live message was never delivered", ); } @@ -454,7 +474,7 @@ proptest! { } let mut obs = known.causal_messages(); - let mut first_run: Vec<(Key, Version, u64)> = Vec::new(); + let mut first_run: Vec<(Version, u64)> = Vec::new(); if complete_drain { first_run.extend(drain(&mut obs).0); } else { @@ -486,17 +506,19 @@ proptest! { first_run .iter() .chain(&second_run) - .any(|(k, _, m)| k == key && m == value), + .any(|(v, m)| v.as_bytes() == key.as_slice() && m == value), "a live message fell between the stopped and resumed observers", ); } // After a complete drain the checkpoint is current: no re-delivery. - let first_keys: BTreeSet = first_run.iter().map(|(k, _, _)| *k).collect(); - let second_keys: BTreeSet = second_run.iter().map(|(k, _, _)| *k).collect(); + let first_versions: BTreeSet> = + first_run.iter().map(|(v, _)| v.as_bytes().to_vec()).collect(); + let second_versions: BTreeSet> = + second_run.iter().map(|(v, _)| v.as_bytes().to_vec()).collect(); if complete_drain { prop_assert!( - first_keys.is_disjoint(&second_keys), + first_versions.is_disjoint(&second_versions), "a drained backlog's messages must not re-fire", ); } @@ -539,7 +561,7 @@ proptest! { let taken = taken % (final_live.len() + 1); let mut causal = known.causal_messages(); - let mut causal_delivered: Vec<(Key, Version, u64)> = Vec::new(); + let mut causal_delivered: Vec<(Version, u64)> = Vec::new(); for _ in 0..taken { match step(&mut causal) { Step::Item(item) => causal_delivered.push(item), @@ -547,33 +569,31 @@ proptest! { } } assert_causal(&causal_delivered); - let causal_checkpoint = - borsh::to_vec(causal.checkpoint()).expect("a checkpoint serializes"); + let causal_checkpoint = causal.checkpoint().as_bytes().to_vec(); let mut unordered = known.unordered_messages(); - let mut unordered_delivered: Vec<(Key, u64)> = Vec::new(); + let mut unordered_delivered: Vec<(Version, u64)> = Vec::new(); for _ in 0..taken { match unordered.try_next() { - rumors::TryNext::Message((key, _, message)) => { - unordered_delivered.push((key, **message)); + rumors::TryNext::Message((version, message)) => { + unordered_delivered.push((version.clone(), **message)); } other => panic!("the pass has more items, got {other:?}"), } } - let unordered_checkpoint = - borsh::to_vec(unordered.checkpoint()).expect("a checkpoint serializes"); + let unordered_checkpoint = unordered.checkpoint().as_bytes().to_vec(); - let causal_handled: BTreeSet = causal_delivered + let causal_handled: BTreeSet> = causal_delivered .iter() .rev() .skip(1) - .map(|(k, _, _)| *k) + .map(|(v, _)| v.as_bytes().to_vec()) .collect(); - let unordered_handled: BTreeSet = unordered_delivered + let unordered_handled: BTreeSet> = unordered_delivered .iter() .rev() .skip(1) - .map(|(k, _)| *k) + .map(|(v, _)| v.as_bytes().to_vec()) .collect(); // The crash: every handle the process held goes away at once. @@ -586,27 +606,31 @@ proptest! { // persisted bytes. let rebuilt = bootstrap_fork(&partner); - let since: Version = - borsh::from_slice(&causal_checkpoint).expect("a checkpoint deserializes"); + let since = + Version::decode(&causal_checkpoint[..]).expect("a checkpoint deserializes"); let mut resumed = rebuilt.causal_messages_since(since); let (replayed, _) = drain(&mut resumed); assert_causal(&replayed); for (key, value) in &final_live { prop_assert!( causal_handled.contains(key) - || replayed.iter().any(|(k, _, m)| k == key && m == value), + || replayed + .iter() + .any(|(v, m)| v.as_bytes() == key.as_slice() && m == value), "causal: unhandled live message {key:?} fell through the restart", ); } - let since: Version = - borsh::from_slice(&unordered_checkpoint).expect("a checkpoint deserializes"); + let since = + Version::decode(&unordered_checkpoint[..]).expect("a checkpoint deserializes"); let mut resumed = rebuilt.unordered_messages_since(since); let replayed = drain_unordered(&mut resumed); for (key, value) in &final_live { prop_assert!( unordered_handled.contains(key) - || replayed.iter().any(|(k, m)| k == key && m == value), + || replayed + .iter() + .any(|(v, m)| v.as_bytes() == key.as_slice() && m == value), "unordered: unhandled live message {key:?} fell through the restart", ); } diff --git a/tests/cbor_evolution.rs b/tests/cbor_evolution.rs new file mode 100644 index 000000000..90a4daa28 --- /dev/null +++ b/tests/cbor_evolution.rs @@ -0,0 +1,177 @@ +//! Pins the payload-evolution contract the CBOR encoding was chosen for: +//! field and variant *names* are the wire contract, not positions. +//! +//! Two struct types with reordered fields, and two enum types with +//! reordered variants, exchange messages end to end — a `Peer` of one type +//! gossiping with a `Peer` of the other over an in-memory link, in both +//! directions — so the property is pinned through the crate's own encode +//! and decode paths, not against a serializer in isolation. A future +//! payload-encoding change that breaks name-keyed decoding (for example, a +//! positional struct encoding) fails these tests loudly. +//! +//! The evolution rules the crate documents ride the same mechanism and are +//! pinned beside it: unknown fields are skipped, and missing fields error +//! unless the field carries `#[serde(default)]`. + +use serde::{Deserialize, Serialize}; + +use rumors::Peer; + +use serde::de::DeserializeOwned; +/// A struct payload in one field order. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +struct WideV1 { + id: u64, + tag: String, + data: Vec, +} + +/// The same struct with its fields reordered: names unchanged, positions +/// scrambled. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +struct WideV2 { + data: Vec, + id: u64, + tag: String, +} + +/// An enum payload in one variant order. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +enum EventV1 { + Ping(u64), + Note { text: String, level: u8 }, + Stop, +} + +/// The same enum with its variants (and one variant's fields) reordered. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +enum EventV2 { + Stop, + Note { level: u8, text: String }, + Ping(u64), +} + +/// Send `payload` from a fresh `Peer` and receive it on a bootstrapped +/// `Peer` over an in-memory link: the crate's whole encode/decode path, +/// across two payload *types*. +async fn exchanged(payload: A) -> B +where + A: Serialize + DeserializeOwned + Send + Sync + 'static, + B: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, +{ + let sender = Peer::::seed().into_rumors(); + sender.send(payload); + + let (mut near, mut far) = rumors::link::memory(); + let serve = sender.clone(); + let server = tokio::spawn(async move { serve.gossip(&mut far).await.unwrap() }); + let receiver = Peer::::bootstrap() + .join(&mut near) + .await + .expect("the bootstrap session succeeds") + .expect("the sender is established") + .into_rumors(); + server.await.expect("the serving task"); + + let snapshot = receiver.snapshot(); + let (_, message) = snapshot.iter().next().expect("one live message"); + (**message).clone() +} + +/// Struct fields decode by name: a payload encoded with one field order +/// decodes into the reordered type with every field intact, in both +/// directions. +#[tokio::test] +async fn reordered_struct_fields_decode_by_name() { + let v1 = WideV1 { + id: 7, + tag: "meeting".to_string(), + data: vec![1, 2, 3], + }; + let v2: WideV2 = exchanged(v1.clone()).await; + assert_eq!(v2.id, v1.id); + assert_eq!(v2.tag, v1.tag); + assert_eq!(v2.data, v1.data); + + let back: WideV1 = exchanged(v2).await; + assert_eq!(back, v1); +} + +/// Enum variants decode by name: a payload encoded against one variant +/// order decodes into the reordered enum — struct-variant fields included — +/// in both directions. +#[tokio::test] +async fn reordered_enum_variants_decode_by_name() { + let note = EventV1::Note { + text: "urgent".to_string(), + level: 3, + }; + let got: EventV2 = exchanged(note).await; + assert_eq!( + got, + EventV2::Note { + level: 3, + text: "urgent".to_string() + } + ); + + let ping: EventV1 = exchanged(EventV2::Ping(99)).await; + assert_eq!(ping, EventV1::Ping(99)); + + let stop: EventV1 = exchanged(EventV2::Stop).await; + assert_eq!(stop, EventV1::Stop); +} + +/// A field the decoder does not know is skipped: a sender speaking a wider +/// struct interoperates with a receiver speaking a narrower one. +#[tokio::test] +async fn unknown_fields_are_skipped() { + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] + struct Narrow { + id: u64, + } + let wide = WideV1 { + id: 42, + tag: "extra".to_string(), + data: vec![9], + }; + let narrow: Narrow = exchanged(wide).await; + assert_eq!(narrow, Narrow { id: 42 }); +} + +/// A missing field errors without `#[serde(default)]` and fills with it: +/// the documented boundary between tolerated and rejected evolution. +#[test] +fn missing_fields_error_absent_a_default() { + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] + struct Narrow { + id: u64, + } + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] + struct Wide { + id: u64, + tag: String, + } + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] + struct WideDefaulted { + id: u64, + #[serde(default)] + tag: String, + } + + let mut narrow = Vec::new(); + ciborium::ser::into_writer(&Narrow { id: 5 }, &mut narrow).unwrap(); + + // Without a default, the absent field is an error, not a guess. + assert!(ciborium::de::from_reader::(narrow.as_slice()).is_err()); + + // With one, the absent field fills in. + let filled: WideDefaulted = ciborium::de::from_reader(narrow.as_slice()).unwrap(); + assert_eq!( + filled, + WideDefaulted { + id: 5, + tag: String::new() + } + ); +} diff --git a/tests/changes.rs b/tests/changes.rs index edb0b2961..603c46c7c 100644 --- a/tests/changes.rs +++ b/tests/changes.rs @@ -10,7 +10,7 @@ mod common; use futures::{FutureExt, StreamExt}; use rumors::{Peer, Rumors}; -use crate::common::action::minted_key; +use crate::common::action::minted_version; use crate::common::wire::{bootstrap_fork_async, wire_gossip_async}; /// A fresh observer yields immediately — even on an empty set — because a @@ -43,12 +43,12 @@ async fn one_tick_per_observed_commit() { assert_eq!(changes.next().now_or_never(), None); // One redact: one tick. - let key = rumors + let version = rumors .snapshot() .iter() - .find_map(|(k, _, m)| (**m == 1).then_some(k)) + .find_map(|(v, m)| (**m == 1).then_some(v.clone())) .expect("message 1 is live"); - rumors.redact(key); + rumors.redact(&version); assert_eq!(changes.next().now_or_never(), Some(Some(()))); assert_eq!(changes.next().now_or_never(), None); } @@ -115,7 +115,7 @@ async fn gossip_frontier_only_advance_ticks_the_observer() { // and the redaction's only trace is A's advanced causal frontier. let pre = a.snapshot().latest().clone(); a.send(7); - a.redact(minted_key(&a.snapshot(), &pre)); + a.redact(&minted_version(&a.snapshot(), &pre)); // B learns that frontier by gossip: verifiably an observed advance of // B's own causal frontier, with no content movement (both trees empty). diff --git a/tests/common/action.rs b/tests/common/action.rs index 923e67714..63cabf7aa 100644 --- a/tests/common/action.rs +++ b/tests/common/action.rs @@ -1,10 +1,11 @@ //! Generic `Insert`/`Redact` action sequences shared by reconciliation tests. -use borsh::{BorshDeserialize, BorshSerialize}; use proptest::collection::vec; use proptest::prelude::*; -use rumors::{Key, Snapshot, Version, causally}; +use rumors::{Snapshot, Version, causally}; +use serde::Serialize; +use serde::de::DeserializeOwned; const MAX_ACTIONS: usize = 16; #[derive(Debug, Clone)] @@ -15,7 +16,8 @@ pub enum LocalAction { /// Strategy over `Vec>`, weighted 4:1 toward inserts. /// `value_strategy` supplies the value type; a `Redact(idx)` picks -/// `keys[idx % len]` at build time (or is dropped if no keys yet). +/// `versions[idx % len]` at build time (or is dropped if nothing has +/// been sent yet). pub fn arb_actions(value_strategy: S) -> impl Strategy>> where T: Clone + std::fmt::Debug + 'static, @@ -42,41 +44,41 @@ pub fn arb_string_actions() -> impl Strategy>> { arb_actions("[a-z]{0,8}".prop_map(String::from)) } -/// Returns the `Key` of the single live leaf in `snapshot` above the -/// causal frontier `pre`. +/// Returns the [`Version`] of the single live leaf in `snapshot` above +/// the causal frontier `pre`. /// -/// This is how a builder recovers the key a `send` just minted, given -/// the `latest()` it recorded before sending. +/// This is how a builder recovers the version a `send` just minted, +/// given the `latest()` it recorded before sending. /// /// # Panics /// /// Panics unless exactly one leaf qualifies. -pub fn minted_key(snapshot: &Snapshot, pre: &Version) -> Key { - let mut fresh = snapshot.range(causally::since(pre)).map(|(k, _, _)| k); - let key = fresh.next().expect("a send mints exactly one live leaf"); +pub fn minted_version(snapshot: &Snapshot, pre: &Version) -> Version { + let mut fresh = snapshot.range(causally::since(pre)).map(|(v, _)| v); + let version = fresh.next().expect("a send mints exactly one live leaf"); assert!( fresh.next().is_none(), "a single send must mint exactly one live leaf" ); - key + version.clone() } /// Apply a `LocalAction` sequence to an already-bootstrapped local replica. pub fn build_local(local: rumors::Rumors, actions: &[LocalAction]) -> rumors::Rumors where - T: Send + Sync + Clone + BorshSerialize + BorshDeserialize + 'static, + T: Send + Sync + Clone + Serialize + DeserializeOwned + 'static, { - let mut keys: Vec = Vec::new(); + let mut versions: Vec = Vec::new(); for a in actions { match a { LocalAction::Insert(v) => { let pre = local.snapshot().latest().clone(); local.send(v.clone()); - keys.push(minted_key(&local.snapshot(), &pre)); + versions.push(minted_version(&local.snapshot(), &pre)); } LocalAction::Redact(idx) => { - if !keys.is_empty() { - local.redact(keys[idx % keys.len()]); + if !versions.is_empty() { + local.redact(&versions[idx % versions.len()]); } } } diff --git a/tests/common/flaky.rs b/tests/common/flaky.rs index 96a7fe0f6..acfcdcd97 100644 --- a/tests/common/flaky.rs +++ b/tests/common/flaky.rs @@ -34,7 +34,7 @@ use tokio::io::AsyncWrite; pub type DurableStore = Arc>>>; /// The fixed-header width of a bookmark frame — magic, the 2-byte format -/// version, and the 32-byte BLAKE3 integrity hash — before the borsh payload. +/// version, and the 32-byte BLAKE3 integrity hash — before the CBOR payload. /// /// Mirrors the crate-private `format::HEADER_LEN`. Integration tests cannot /// reach the crate's codec, so they strip this known header to read the payload; @@ -42,11 +42,11 @@ pub type DurableStore = Arc>>>; const FRAME_HEADER_LEN: usize = BOOKMARK_MAGIC.len() + 2 + 32; /// Decode the record a persisted store holds, or an empty record if nothing has -/// been written. Strips the crate's frame header and borsh-decodes the payload. +/// been written. Strips the crate's frame header and CBOR-decodes the payload. pub fn persisted_record(store: &DurableStore) -> BTreeMap> { match &*store.lock().unwrap() { None => BTreeMap::new(), - Some(bytes) => borsh::from_slice(&bytes[FRAME_HEADER_LEN..]) + Some(bytes) => ciborium::de::from_reader(&bytes[FRAME_HEADER_LEN..]) .expect("decode persisted bookmark payload"), } } diff --git a/tests/common/gossip_snapshot.rs b/tests/common/gossip_snapshot.rs index 9825c1afc..20233a5bf 100644 --- a/tests/common/gossip_snapshot.rs +++ b/tests/common/gossip_snapshot.rs @@ -41,7 +41,6 @@ use std::pin::Pin; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; -use borsh::{BorshDeserialize, BorshSerialize}; use rumors::link::{Connector, Done, Link, LinkParts, MemoryAcceptor, MemoryConnector}; use rumors::{ Rumors, @@ -51,6 +50,8 @@ use tokio::io::{AsyncRead, AsyncWrite, DuplexStream, ReadBuf}; use crate::common::wire::block_on; +use serde::Serialize; +use serde::de::DeserializeOwned; /// Whether a logged byte run was put on the wire or taken off it, from the /// perspective of the peer that performed the I/O. #[derive(Clone, Copy, PartialEq, Eq)] @@ -364,7 +365,7 @@ where /// reconcile cleanly; a gossip error panics the helper. pub fn capture_gossip(a: Rumors, b: Rumors) -> String where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { capture_session( move |mut link| async move { @@ -379,7 +380,7 @@ where /// Capture the strict V1 timeline for a gossip/gossip session. pub fn capture_gossip_v1(a: Rumors, b: Rumors) -> String where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { capture_session_v1( move |mut link| async move { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index cbc7085a0..a558a564e 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -41,6 +41,7 @@ pub mod overlap; pub mod peer; pub mod routed_tcp; pub mod schedule; +pub mod shape; pub mod sim; pub mod tcp; pub mod window; diff --git a/tests/common/oracle.rs b/tests/common/oracle.rs index ff11953fe..507032e56 100644 --- a/tests/common/oracle.rs +++ b/tests/common/oracle.rs @@ -1,16 +1,16 @@ //! Spec-shaped oracle for the gossip-set semantics, plus a `readout` //! lens that projects a [`Snapshot`] back into its currently-live -//! `(Key, T)` map. +//! map from message identity (a version's canonical bytes) to value. //! //! The oracle holds only `BTreeMap`s and `BTreeSet`s (no rumor set, no //! merging), so a bug in the live merge primitives cannot silently corrupt //! the reference state. It records each insert by //! the schedule's [`EventIdx`] so the oracle and the live executor -//! agree on identity without ever consulting the live `Key`s. +//! agree on identity without ever consulting the live [`Version`]s. use std::collections::{BTreeMap, BTreeSet}; -use rumors::{Key, Snapshot}; +use rumors::{Snapshot, Version}; use super::schedule::EventIdx; @@ -49,10 +49,8 @@ impl Oracle { } /// Every insert the oracle has seen, redacted or not, as - /// `EventIdx → value`. Used by [`multi_peer::keys_stable_across_peers`] - /// to build the canonical `Key → value` map. - /// - /// [`multi_peer::keys_stable_across_peers`]: crate::multi_peer + /// `EventIdx → value`. Used to build the canonical identity → value + /// map that cross-peer suites compare against. pub fn all_inserts(&self) -> &BTreeMap { &self.values } @@ -62,19 +60,30 @@ impl Oracle { } } -/// Project a [`Snapshot`] into its currently-live `(Key, T)` map. +/// A message's identity as an orderable map key: its [`Version`]'s +/// canonical bytes. +/// +/// Canonical and injective, so equality of byte keys is equality of +/// versions; the lexicographic order is an arbitrary total order +/// ([`Version`] itself is only partially ordered). +pub fn version_key(version: &Version) -> Vec { + version.as_bytes().to_vec() +} + +/// Project a [`Snapshot`] into its currently-live identity → value +/// map, keyed by each message's [`version_key`]. /// /// A direct read via [`Snapshot::iter`]: it enumerates exactly the live /// leaves, so redacted messages — whose leaves the redaction *removed*, /// leaving no marker — are simply absent. Taking the [`Snapshot`] rather /// than a live handle also keeps this oracle independent of observer state. -pub fn readout(snapshot: &Snapshot) -> BTreeMap +pub fn readout(snapshot: &Snapshot) -> BTreeMap, T> where T: Clone + Send + Sync + 'static, { snapshot .iter() - .map(|(k, _v, m)| (k, (**m).clone())) + .map(|(v, m)| (version_key(v), (**m).clone())) .collect() } diff --git a/tests/common/overlap.rs b/tests/common/overlap.rs index cf43e3193..ed94ec556 100644 --- a/tests/common/overlap.rs +++ b/tests/common/overlap.rs @@ -22,7 +22,7 @@ //! generator keeps the schedule valid by construction the same way //! [`schedule::arb`] does — a shadow simulator tracks what every peer has //! observed, with open sessions modeled by their fork-time snapshots so a -//! `Redact` is only ever emitted against a key its peer really holds. +//! `Redact` is only ever emitted against a message its peer really holds. use std::collections::BTreeMap; use std::fmt::Debug; @@ -31,17 +31,18 @@ use std::ops::RangeInclusive; use std::pin::Pin; use std::task::{Context, Poll, Waker}; -use borsh::{BorshDeserialize, BorshSerialize}; use proptest::collection::vec; use proptest::prelude::*; use rumors::link::memory_with_capacity; -use rumors::{Key, Rumors}; +use rumors::{Rumors, Version}; use crate::common::oracle::Oracle; use crate::common::peer::{Peer, gossip_step, quiesce}; use crate::common::schedule::EventIdx; use crate::common::wire::bootstrap_fork; +use serde::Serialize; +use serde::de::DeserializeOwned; /// Capacity of an overlapped session's link streams, in bytes. /// /// Deliberately tiny, unlike [`wire::LINK_BUF`](crate::common::wire::LINK_BUF): @@ -91,7 +92,7 @@ pub struct Session { /// Open a wire gossip session between `a` and `b` without polling it. pub fn open(a: &Rumors, b: &Rumors) -> Session where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { let a = a.clone(); let b = b.clone(); @@ -164,9 +165,9 @@ impl Session { pub enum OverlapEvent { /// Insert `value` at `peer`. Insert { peer: usize, value: T }, - /// Redact the key minted by the `Insert` at `target_event_idx`. + /// Redact the message minted by the `Insert` at `target_event_idx`. /// Valid by construction: the generator's shadow guarantees `peer` - /// has observed that key when this event runs. + /// has observed that message when this event runs. Redact { peer: usize, target_event_idx: EventIdx, @@ -200,7 +201,7 @@ pub struct OverlapSchedule { /// two agree. pub fn execute_overlap_and_quiesce(schedule: &OverlapSchedule) -> (Vec>, Oracle) where - T: Clone + Eq + Ord + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Eq + Ord + Serialize + DeserializeOwned + Send + Sync + 'static, { let mut peers: Vec> = Vec::with_capacity(schedule.n_peers); for i in 0..schedule.n_peers { @@ -212,7 +213,7 @@ where peers.push(Peer::new(local)); } let mut oracle = Oracle::::default(); - let mut resolved_keys: BTreeMap = BTreeMap::new(); + let mut resolved_versions: BTreeMap = BTreeMap::new(); // Open sessions, keyed by slot, with their endpoints retained so both // observation logs can drain when the session closes. let mut open_sessions: BTreeMap = BTreeMap::new(); @@ -220,22 +221,22 @@ where for (i, event) in schedule.events.iter().enumerate() { match event { OverlapEvent::Insert { peer, value } => { - let key = peers[*peer].insert_one(value.clone()); - resolved_keys.insert(i, key); + let version = peers[*peer].insert_one(value.clone()); + resolved_versions.insert(i, version); oracle.insert(i, value.clone()); } OverlapEvent::Redact { peer, target_event_idx, } => { - let key = resolved_keys[target_event_idx]; + let version = &resolved_versions[target_event_idx]; // The generator's shadow makes this always-observed; the // guard mirrors the serial executor's, so a shadow // imprecision degrades to a skipped event on both sides // of the comparison rather than an invalid `redact`. - let observed = peers[*peer].observations.iter().any(|(k, _, _)| *k == key); + let observed = peers[*peer].observations.iter().any(|(v, _)| v == version); if observed { - peers[*peer].redact_one(key); + peers[*peer].redact_one(version); oracle.redact(*target_event_idx); } } @@ -522,13 +523,14 @@ impl Knowledge { /// Merge what a session forked at `snapshot` delivers between `a` /// and `b` into the *current* state. /// - /// The session carries each side's fork-time content only: keys one - /// fork-time side held live propagate to a counterparty that has - /// never known them; keys either fork-time side had redacted die on - /// both current sides (deletion honoring, tombstone-free). A key - /// redacted *after* the fork stays dead locally — `ever_known` - /// guards resurrection — and its counterparty learns that deletion - /// only from a later session, exactly as the wire behaves. + /// The session carries each side's fork-time content only: messages + /// one fork-time side held live propagate to a counterparty that has + /// never known them; messages either fork-time side had redacted die + /// on both current sides (deletion honoring, tombstone-free). A + /// message redacted *after* the fork stays dead locally — + /// `ever_known` guards resurrection — and its counterparty learns + /// that deletion only from a later session, exactly as the wire + /// behaves. fn merge_session(&mut self, snapshot: &Knowledge, a: usize, b: usize) { let combined: std::collections::BTreeSet = snapshot.ever_known[a] .union(&snapshot.ever_known[b]) @@ -606,9 +608,9 @@ fn build_overlap_schedule( continue; } let target_event_idx = log[idx % log.len()]; - // Only keys still live locally are sensible targets; a - // second redact of the same key is a no-op the executor - // would skip asymmetrically. + // Only messages still live locally are sensible targets; + // a second redact of the same message is a no-op the + // executor would skip asymmetrically. if !sim.live[peer].contains(&target_event_idx) { continue; } diff --git a/tests/common/peer.rs b/tests/common/peer.rs index be7b2eb64..47517c7a7 100644 --- a/tests/common/peer.rs +++ b/tests/common/peer.rs @@ -13,11 +13,12 @@ //! observed, matching both the `UnorderedMessages` delivery contract and //! the shadow simulator's model in `schedule::arb`. -use borsh::{BorshDeserialize, BorshSerialize}; -use rumors::{Key, Rumors, Version, causally}; +use rumors::{Rumors, Version, causally}; use crate::common::wire::{block_on, wire_gossip_async}; +use serde::Serialize; +use serde::de::DeserializeOwned; /// One simulated peer. pub struct Peer { pub local: Rumors, @@ -32,10 +33,10 @@ pub struct Peer { /// Drain order within a pass is the tree's iteration order; in practice /// it is deterministic across runs, so the log is reproducible inside a /// counterexample. - pub observations: Vec<(Key, Version, T)>, + pub observations: Vec<(Version, T)>, } -impl Peer { +impl Peer { /// Wrap an already-forked `Rumors` as a simulated peer. Observation /// starts at the wrapped set's current frontier: content already present /// is never logged, only what arrives afterwards. @@ -55,7 +56,7 @@ impl Peer< /// Snapshot of the observation log, in insertion order. Convenience /// for tests that read out `peer.observations` for assertions. - pub fn observations(&self) -> Vec<(Key, Version, T)> { + pub fn observations(&self) -> Vec<(Version, T)> { self.observations.clone() } @@ -64,29 +65,29 @@ impl Peer< pub fn drain(&mut self) -> usize { let snapshot = self.local.snapshot(); let mut new = 0; - for (key, version, message) in snapshot.range(causally::since(&self.checkpoint)) { + for (version, message) in snapshot.range(causally::since(&self.checkpoint)) { self.observations - .push((key, version.clone(), (**message).clone())); + .push((version.clone(), (**message).clone())); new += 1; } self.checkpoint |= snapshot.latest(); new } - /// Insert a single value, returning the `Key` minted for it. - pub fn insert_one(&mut self, value: T) -> Key { + /// Insert a single value, returning the [`Version`] minted for it. + pub fn insert_one(&mut self, value: T) -> Version { // Catch the log up first, so the send's drain isolates exactly the - // one new observation and its key. + // one new observation and its version. self.drain(); self.local.send(value); let pre = self.observations.len(); let drained = self.drain(); assert_eq!(drained, 1, "a send mints exactly one new observation"); - self.observations[pre].0 + self.observations[pre].0.clone() } - pub fn redact_one(&mut self, key: Key) { - self.local.redact(key); + pub fn redact_one(&mut self, version: &Version) { + self.local.redact(version); // Redactions fire no observation; the drain just absorbs the // version tick into the checkpoint. self.drain(); @@ -98,7 +99,7 @@ impl Peer< /// version, and both observation logs have caught up. pub fn gossip_step(a: &mut Peer, b: &mut Peer) where - T: Clone + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Serialize + DeserializeOwned + Send + Sync + 'static, { block_on(wire_gossip_async(&a.local, &b.local)); a.drain(); @@ -111,7 +112,7 @@ where /// non-termination guard. pub fn quiesce(peers: &mut [Peer]) where - T: Clone + Eq + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Eq + Serialize + DeserializeOwned + Send + Sync + 'static, { let mut refs: Vec<&mut Peer> = peers.iter_mut().collect(); quiesce_refs(&mut refs); @@ -121,7 +122,7 @@ where /// slotted fleet, skipping retired peers' vacated slots. pub fn quiesce_slots(slots: &mut [Option>]) where - T: Clone + Eq + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Eq + Serialize + DeserializeOwned + Send + Sync + 'static, { let mut refs: Vec<&mut Peer> = slots.iter_mut().filter_map(Option::as_mut).collect(); quiesce_refs(&mut refs); @@ -138,7 +139,7 @@ where /// should catch). fn quiesce_refs(peers: &mut [&mut Peer]) where - T: Clone + Eq + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Eq + Serialize + DeserializeOwned + Send + Sync + 'static, { let n = peers.len(); if n < 2 { diff --git a/tests/common/schedule/arb.rs b/tests/common/schedule/arb.rs index 3447b601d..d8bfdcb8c 100644 --- a/tests/common/schedule/arb.rs +++ b/tests/common/schedule/arb.rs @@ -3,7 +3,7 @@ //! //! Every schedule emitted by [`arb_schedule`] and //! [`arb_membership_schedule`] is *valid by construction*: a `Redact` -//! event always references an `Insert` whose `Key` the redacting peer +//! event always references an `Insert` whose message the redacting peer //! has already observed by that point, and every event names only peers //! alive when it runs (a `Retire` needs two distinct live peers). To //! enforce this, the generator drives a [`SimState`] in lockstep with @@ -178,7 +178,7 @@ enum Choice { peer: usize, value: T, }, - /// Pick the `idx % len`-th key in the redacting peer's current + /// Pick the `idx % len`-th entry in the redacting peer's current /// observation log; if the log is empty, the choice is dropped. RedactObservation { peer: usize, @@ -242,7 +242,7 @@ where /// live simulation would observe under the actual protocol. For peer /// `p`: /// -/// * `ever_known[p]` is every `EventIdx` whose `Key` `p` has ever +/// * `ever_known[p]` is every `EventIdx` whose message `p` has ever /// held (whether it currently holds it or has since redacted it). /// * `live[p]` is the subset currently in `p`'s live rumor set. /// * `observed_log[p]` is the exact sequence of `EventIdx`s that the @@ -250,7 +250,7 @@ where /// this point — driven by both local inserts and gossip events. /// /// `RedactObservation` picks an entry from `observed_log` to redact, -/// so the schedule is guaranteed to issue every `Redact` on a `Key` +/// so the schedule is guaranteed to issue every `Redact` on a message /// the peer actually holds at that moment. struct SimState { ever_known: Vec>, @@ -306,9 +306,9 @@ impl SimState { } /// One direction of a reconciliation: `dst` ends holding the union - /// of both contents — it learns `src`'s novel live keys (observing - /// them) and either side's redaction prevails in `dst` — while `src` - /// is untouched. + /// of both contents — it learns `src`'s novel live messages + /// (observing them) and either side's redaction prevails in `dst` — + /// while `src` is untouched. /// /// A full gossip is an absorb each way; a retirement is one absorb /// into the survivor. @@ -344,7 +344,7 @@ impl SimState { fn record_redact(&mut self, peer: usize, target_event_idx: EventIdx) { // Removing from live (the peer's act of forgetting). // `ever_known` and `observed_log` are unchanged: the peer - // still remembers that it once held this key. + // still remembers that it once held this message. self.live[peer].remove(&target_event_idx); } @@ -357,7 +357,7 @@ impl SimState { // point (the second pass reads the first's updates, so a // redaction on either side prevails in both), and per-peer // observation order is unchanged: each side still gains novel - // keys in sorted combined order. + // messages in sorted combined order. self.absorb(a, b); self.absorb(b, a); } @@ -401,7 +401,7 @@ fn build_schedule( target_event_idx, }); } - // else: the peer has not yet observed any key, so no + // else: the peer has not yet observed anything, so no // application code path could have produced this // `redact()` call. Drop the choice. } diff --git a/tests/common/schedule/events.rs b/tests/common/schedule/events.rs index 47cacfe80..43a77e750 100644 --- a/tests/common/schedule/events.rs +++ b/tests/common/schedule/events.rs @@ -11,10 +11,12 @@ pub enum Event { peer: usize, value: T, }, - /// Redact the `Key` minted by the `Insert` event at this index in - /// the schedule's emitted event sequence. The strategy guarantees - /// the redacting peer has observed that `Key` by the time this - /// event runs. + /// Redact the message (by its minted `Version`) sent by the + /// `Insert` event at this index in the schedule's emitted event + /// sequence. + /// + /// The strategy guarantees the redacting peer has observed that + /// message by the time this event runs. Redact { peer: usize, target_event_idx: EventIdx, diff --git a/tests/common/schedule/executor.rs b/tests/common/schedule/executor.rs index 675b68913..393f891fd 100644 --- a/tests/common/schedule/executor.rs +++ b/tests/common/schedule/executor.rs @@ -11,8 +11,7 @@ use std::collections::BTreeMap; -use borsh::{BorshDeserialize, BorshSerialize}; -use rumors::{Key, Retire, Version}; +use rumors::{Retire, Version}; use super::events::{Event, EventIdx, Schedule}; use crate::common::oracle::Oracle; @@ -20,16 +19,19 @@ use crate::common::peer::{Peer, gossip_step, quiesce, quiesce_slots}; use crate::common::window::WindowAssignment; use crate::common::wire::{LINK_BUF, assert_control_drained, block_on, bootstrap_fork_with_window}; +use serde::Serialize; +use serde::de::DeserializeOwned; pub struct ExecutionResult { pub peers: Vec>, pub oracle: Oracle, - /// For each `Insert` event, the `Key` minted at the originating peer. - pub resolved_keys: BTreeMap, + /// For each `Insert` event, the [`Version`] minted at the originating + /// peer. + pub resolved_versions: BTreeMap, } /// What executing a membership schedule leaves behind: the fleet as /// slots (a retired peer's slot is `None`), every retiree's complete -/// observation log, and the same oracle and key map as the +/// observation log, and the same oracle and version map as the /// membership-free result. pub struct MembershipExecutionResult { /// One slot per peer ever minted — the initial fleet, then every @@ -37,10 +39,11 @@ pub struct MembershipExecutionResult { pub slots: Vec>>, /// Each retired peer's observation log, complete as of the drain /// that preceded its retirement. - pub retired_observations: BTreeMap>, + pub retired_observations: BTreeMap>, pub oracle: Oracle, - /// For each `Insert` event, the `Key` minted at the originating peer. - pub resolved_keys: BTreeMap, + /// For each `Insert` event, the [`Version`] minted at the originating + /// peer. + pub resolved_versions: BTreeMap, } impl MembershipExecutionResult { @@ -62,7 +65,7 @@ impl MembershipExecutionResult { /// between differently-configured endpoints). pub fn execute(schedule: &Schedule, windows: &WindowAssignment) -> ExecutionResult where - T: Clone + Ord + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Ord + Serialize + DeserializeOwned + Send + Sync + 'static, { execute_with(schedule, windows, |_, _, _| true) } @@ -75,7 +78,7 @@ pub fn execute_and_quiesce( windows: &WindowAssignment, ) -> ExecutionResult where - T: Clone + Eq + Ord + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Eq + Ord + Serialize + DeserializeOwned + Send + Sync + 'static, { let mut result = execute(schedule, windows); quiesce(&mut result.peers); @@ -93,8 +96,8 @@ where /// guarantee for `Redact` events no longer holds: a `Redact` whose /// target the peer has not yet observed in this run is silently /// skipped (and the oracle does not record it), which models real -/// usage — application code can only `redact()` a `Key` it has been -/// handed. +/// usage — application code can only `redact()` a [`Version`] it has +/// been handed. /// /// # Panics /// @@ -109,7 +112,7 @@ pub fn execute_with( allow_gossip: F, ) -> ExecutionResult where - T: Clone + Ord + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Ord + Serialize + DeserializeOwned + Send + Sync + 'static, F: Fn(usize, usize, EventIdx) -> bool, { assert!( @@ -128,7 +131,7 @@ where ExecutionResult { peers, oracle: result.oracle, - resolved_keys: result.resolved_keys, + resolved_versions: result.resolved_versions, } } @@ -138,7 +141,7 @@ pub fn execute_membership( windows: &WindowAssignment, ) -> MembershipExecutionResult where - T: Clone + Ord + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Ord + Serialize + DeserializeOwned + Send + Sync + 'static, { execute_slots(schedule, windows, |_, _, _| true) } @@ -150,7 +153,7 @@ pub fn execute_membership_and_quiesce( windows: &WindowAssignment, ) -> MembershipExecutionResult where - T: Clone + Eq + Ord + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Eq + Ord + Serialize + DeserializeOwned + Send + Sync + 'static, { let mut result = execute_membership(schedule, windows); quiesce_slots(&mut result.slots); @@ -175,7 +178,7 @@ fn execute_slots( allow_gossip: F, ) -> MembershipExecutionResult where - T: Clone + Ord + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Ord + Serialize + DeserializeOwned + Send + Sync + 'static, F: Fn(usize, usize, EventIdx) -> bool, { let mut slots: Vec>> = Vec::with_capacity(schedule.n_peers); @@ -190,31 +193,31 @@ where }; slots.push(Some(Peer::new(local))); } - let mut retired_observations: BTreeMap> = BTreeMap::new(); + let mut retired_observations: BTreeMap> = BTreeMap::new(); let mut oracle = Oracle::::default(); - let mut resolved_keys: BTreeMap = BTreeMap::new(); + let mut resolved_versions: BTreeMap = BTreeMap::new(); for (i, event) in schedule.events.iter().enumerate() { match event { Event::Insert { peer, value } => { let peer = slots[*peer].as_mut().expect("insert names an alive peer"); - let key = peer.insert_one(value.clone()); - resolved_keys.insert(i, key); + let version = peer.insert_one(value.clone()); + resolved_versions.insert(i, version); oracle.insert(i, value.clone()); } Event::Redact { peer, target_event_idx, } => { - let key = resolved_keys[target_event_idx]; + let version = &resolved_versions[target_event_idx]; let peer = slots[*peer].as_mut().expect("redact names an alive peer"); - let observed_locally = peer.observations.iter().any(|(k, _, _)| *k == key); + let observed_locally = peer.observations.iter().any(|(v, _)| v == version); if observed_locally { - peer.redact_one(key); + peer.redact_one(version); oracle.redact(*target_event_idx); } // else: under a gossip filter, this peer may not yet - // have observed the key. Real application code + // have observed the version. Real application code // couldn't issue this redact, so skip it. } Event::Gossip { a, b } => { @@ -279,6 +282,6 @@ where slots, retired_observations, oracle, - resolved_keys, + resolved_versions, } } diff --git a/tests/common/shape.rs b/tests/common/shape.rs new file mode 100644 index 000000000..a2dffafb5 --- /dev/null +++ b/tests/common/shape.rs @@ -0,0 +1,100 @@ +//! Deterministic tree-shape staging for the wire-pin fixtures. +//! +//! A leaf's tree path is a pure function of its version, and a staged +//! universe's versions are a pure function of the staging script (the +//! seeded network, the fork points, the send order) — payload bytes steer +//! nothing. A fixture therefore stages a required shape in three steps: +//! mint a pool of messages, search the minted versions for ones whose +//! paths satisfy the shape, and redact the rest. Same script, same +//! versions, same winners, every run: the searches here are deterministic, +//! and each fixture's self-checks still verify the landed shape. + +use rumors::{Rumors, Version}; + +/// A leaf's tree path: the full-width BLAKE3 hash of its version's +/// canonical bytes. +pub fn leaf_path(version: &Version) -> [u8; 32] { + *blake3::hash(version.as_bytes()).as_bytes() +} + +/// The root radix of a leaf's path: its first byte. +pub fn path_radix(version: &Version) -> u8 { + leaf_path(version)[0] +} + +/// Send `count` messages carrying the payloads `from..from + count`, as +/// one batch: one fresh version per payload, in payload order. +pub fn send_pool(rumors: &Rumors, from: u64, count: u64) { + let mut batch = rumors.batch(); + for value in from..from + count { + batch.send(value); + } +} + +/// The live pool as `(payload, version)` in ascending payload order, +/// restricted to payloads in `from..from + count`: the deterministic +/// search order for the shape searches. +pub fn pool(rumors: &Rumors, from: u64, count: u64) -> Vec<(u64, Version)> { + let mut pool: Vec<(u64, Version)> = rumors + .snapshot() + .iter() + .filter(|(_, m)| (from..from + count).contains(m)) + .map(|(v, m)| (**m, v.clone())) + .collect(); + pool.sort_by_key(|(value, _)| *value); + pool +} + +/// Redact every live message whose payload lies in `from..from + count` +/// but is not listed in `keep`: the pool cleanup after a shape search. +pub fn keep_only(rumors: &Rumors, from: u64, count: u64, keep: &[u64]) { + let losers: Vec = rumors + .snapshot() + .iter() + .filter(|(_, m)| (from..from + count).contains(m) && !keep.contains(m)) + .map(|(v, _)| v.clone()) + .collect(); + let mut batch = rumors.batch(); + for version in &losers { + batch.redact(version); + } +} + +/// The first pool pair (in payload order) whose paths agree on the +/// leading `shared` bytes; `distinct_next` additionally requires the byte +/// after the shared span to differ (a split exactly one level below). +/// +/// Panics if the pool holds no such pair: enlarge the pool — the verdict +/// is deterministic, never flaky. +pub fn shaped_pair(pool: &[(u64, Version)], shared: usize, distinct_next: bool) -> (u64, u64) { + for (i, (first, v1)) in pool.iter().enumerate() { + let p1 = leaf_path(v1); + for (second, v2) in &pool[i + 1..] { + let p2 = leaf_path(v2); + if p1[..shared] == p2[..shared] && (!distinct_next || p1[shared] != p2[shared]) { + return (*first, *second); + } + } + } + panic!("no pool pair shares a {shared}-byte path prefix: enlarge the pool"); +} + +/// The first `count` pool payloads whose root radix is not `avoid`: +/// ballast that stays out of a fixture's disputed subtree. +/// +/// Panics if the pool runs dry: enlarge it — the verdict is +/// deterministic, never flaky. +pub fn ballast_avoiding(pool: &[(u64, Version)], avoid: u8, count: usize) -> Vec { + let picked: Vec = pool + .iter() + .filter(|(_, v)| path_radix(v) != avoid) + .take(count) + .map(|(value, _)| *value) + .collect(); + assert_eq!( + picked.len(), + count, + "the pool cannot fill the ballast quota: enlarge it" + ); + picked +} diff --git a/tests/common/sim.rs b/tests/common/sim.rs index e0652d43c..90a6cafb3 100644 --- a/tests/common/sim.rs +++ b/tests/common/sim.rs @@ -25,8 +25,8 @@ //! one observer of each kind //! ([`UnorderedMessages`](rumors::UnorderedMessages) and //! [`CausalMessages`](rumors::CausalMessages)), drained concurrently -//! with the chaos and asserting the delivery contracts inline — no key -//! twice, no causal inversion, and full coverage of the peer's live set +//! with the chaos and asserting the delivery contracts inline — no +//! message twice, no causal inversion, and full coverage of the peer's live set //! once the writers settle (see [`run_observers`]). This is the only //! place the observers' watch-coalescing path runs against genuinely //! parallel writers. @@ -46,7 +46,7 @@ //! execution time by [`run_activity`] — execution time because a //! [`Activity::Redact`] resolves its target against the peer's snapshot //! only when it runs, so no pre-run analysis of the plan can know which -//! `(Key, value)` it removed. [`SimOutcome`] carries both sides of the +//! `(Version, value)` it removed. [`SimOutcome`] carries both sides of the //! ledger; [`assert_deletion_honored`] and [`assert_value_oracle`] check //! the converged fleet against it. //! @@ -80,10 +80,10 @@ use proptest::prelude::*; use rumors::error::{ CodecDecodeErrorKind, CodecEncodeErrorKind, RemoteError, SendError, StreamError, }; -use rumors::{Error, Key, MirrorError, Peer, Retire, Rumors, Version}; +use rumors::{Error, MirrorError, Peer, Retire, Rumors, Version}; use crate::common::fault::{self, FaultPlan}; -use crate::common::oracle::{readout, readout_multiset}; +use crate::common::oracle::{readout, readout_multiset, version_key}; use crate::common::window::{WindowAssignment, WindowChoice, arb_window_choice}; use crate::common::wire::wire_gossip_async; @@ -141,9 +141,9 @@ pub struct Plan { pub enum Activity { /// Insert this value. Send(u64), - /// Redact the key at this index (modulo the live count) of the peer's - /// own snapshot at execution time — a key the application could have - /// observed; a no-op while the peer holds nothing. + /// Redact the message at this index (modulo the live count) of the + /// peer's own snapshot at execution time — a message the application + /// could have observed; a no-op while the peer holds nothing. Redact(usize), } @@ -167,14 +167,14 @@ pub struct RetireOp { } /// One executed redaction, logged by [`run_activity`] at the moment it -/// resolved its target, and deduplicated per [`Key`] (two peers racing to -/// redact the same key are one redaction of it). -#[derive(Debug, Clone, Copy)] +/// resolved its target, and deduplicated per [`Version`] (two peers racing +/// to redact the same message are one redaction of it). +#[derive(Debug, Clone)] pub struct Redaction { - /// The key actually redacted. - pub key: Key, - /// The value that key carried, read from the redactor's snapshot in - /// the same pass that selected the key. + /// The version of the message actually redacted. + pub version: Version, + /// The value that message carried, read from the redactor's snapshot + /// in the same pass that selected it. pub value: u64, /// Whether the redaction is guaranteed present in the surviving /// fleet's causal history. @@ -510,15 +510,15 @@ async fn run_boot( /// Run one peer's activity script, yielding between operations so it /// interleaves with every in-flight session. /// -/// Returns the `(Key, value)` of every redaction actually executed: the -/// target resolves against the peer's snapshot only here, so this +/// Returns the `(Version, value)` of every redaction actually executed: +/// the target resolves against the peer's snapshot only here, so this /// execution-time log is the one ground truth of what the plan redacted /// (the value oracle's deletion side; see the module docs). /// -/// A logged key may race a sibling's redaction of the same key; either -/// way the key ends redacted network-wide, so the log stays sound and -/// [`run_plan`] deduplicates by key. -async fn run_activity(handle: Rumors, script: Vec) -> Vec<(Key, u64)> { +/// A logged redaction may race a sibling's redaction of the same message; +/// either way the message ends redacted network-wide, so the log stays +/// sound and [`run_plan`] deduplicates by version. +async fn run_activity(handle: Rumors, script: Vec) -> Vec<(Version, u64)> { let mut redacted = Vec::new(); for op in script { match op { @@ -526,12 +526,15 @@ async fn run_activity(handle: Rumors, script: Vec) -> Vec<(Key, u handle.send(value); } Activity::Redact(index) => { - let live: Vec<(Key, u64)> = - handle.snapshot().iter().map(|(k, _, m)| (k, **m)).collect(); + let live: Vec<(Version, u64)> = handle + .snapshot() + .iter() + .map(|(v, m)| (v.clone(), **m)) + .collect(); if !live.is_empty() { - let (key, value) = live[index % live.len()]; - handle.redact(key); - redacted.push((key, value)); + let (version, value) = live[index % live.len()].clone(); + handle.redact(&version); + redacted.push((version, value)); } } } @@ -543,11 +546,11 @@ async fn run_activity(handle: Rumors, script: Vec) -> Vec<(Key, u /// Drain one peer's observers — one of each kind — concurrently with the /// chaos, asserting the delivery contracts on every step: /// -/// - **Exactly-once**: neither observer ever yields a key twice. +/// - **Exactly-once**: neither observer ever yields a message twice. /// - **Causal order** (the causal observer): no delivery is ever a causal /// predecessor of an earlier one. /// - **Coverage**: once `done` (the writers have settled), a final drain -/// leaves every key live in the peer's snapshot observed by both. +/// leaves every message live in the peer's snapshot observed by both. /// /// The interesting part is not the assertions but where they run: under /// genuinely parallel sends, redactions, and gossip sessions on sibling @@ -559,8 +562,8 @@ async fn run_observers(handle: Rumors, done: Arc) { let mut plain = handle.unordered_messages(); let mut causal = handle.causal_messages(); - let mut plain_seen: BTreeSet = BTreeSet::new(); - let mut causal_seen: BTreeSet = BTreeSet::new(); + let mut plain_seen: BTreeSet> = BTreeSet::new(); + let mut causal_seen: BTreeSet> = BTreeSet::new(); let mut causal_delivered: Vec = Vec::new(); loop { @@ -569,17 +572,16 @@ async fn run_observers(handle: Rumors, done: Arc) { // races nothing. let finished = done.load(Ordering::Acquire); - while let Some(Some((key, version, _))) = plain.borrow_next().now_or_never() { - let _ = version; + while let Some(Some((version, _))) = plain.borrow_next().now_or_never() { assert!( - plain_seen.insert(key), - "Messages delivered key {key:?} twice" + plain_seen.insert(version_key(version)), + "Messages delivered version {version:?} twice" ); } - while let Some(Some((key, version, _))) = causal.borrow_next().now_or_never() { + while let Some(Some((version, _))) = causal.borrow_next().now_or_never() { assert!( - causal_seen.insert(key), - "CausalMessages delivered key {key:?} twice" + causal_seen.insert(version_key(version)), + "CausalMessages delivered version {version:?} twice" ); // `Version` is a partial order: `!(version < earlier)` also // admits concurrent pairs, which `version >= earlier` would @@ -603,14 +605,14 @@ async fn run_observers(handle: Rumors, done: Arc) { // The writers have settled and both observers are quiet: everything // live in the set was live at each observer's final pass. - for (key, _, _) in handle.snapshot().iter() { + for (version, _) in handle.snapshot().iter() { assert!( - plain_seen.contains(&key), - "Messages never delivered live key {key:?}" + plain_seen.contains(version.as_bytes()), + "Messages never delivered live version {version:?}" ); assert!( - causal_seen.contains(&key), - "CausalMessages never delivered live key {key:?}" + causal_seen.contains(version.as_bytes()), + "CausalMessages never delivered live version {version:?}" ); } } @@ -724,7 +726,7 @@ pub async fn run_plan(plan: Plan) -> SimOutcome { }) .collect(); // Per-founder execution-time redaction logs, indexed like `casts`. - let mut redaction_logs: Vec> = Vec::with_capacity(activity_tasks.len()); + let mut redaction_logs: Vec> = Vec::with_capacity(activity_tasks.len()); for task in activity_tasks { redaction_logs.push(task.await.expect("activity task")); } @@ -833,23 +835,24 @@ pub async fn run_plan(plan: Plan) -> SimOutcome { ); } - // Deduplicate the redaction ledger by key: racing redactors of one key - // are one redaction of it, retained if *any* logger's final content - // reached the surviving fleet. Content addressing makes the key→value - // binding immutable, so colliding logs always agree on the value. + // Deduplicate the redaction ledger by version: racing redactors of one + // message are one redaction of it, retained if *any* logger's final + // content reached the surviving fleet. A version names exactly one + // message, so colliding logs always agree on the value. let lost_founders = lost_custody(plan.n_peers, &transfers); - let mut by_key: BTreeMap = BTreeMap::new(); + let mut by_version: BTreeMap, Redaction> = BTreeMap::new(); for (founder, log) in redaction_logs.iter().enumerate() { let retained = !lost_founders.contains(&founder); - for &(key, value) in log { - let entry = by_key.entry(key).or_insert(Redaction { - key, - value, + for (version, value) in log { + let entry = by_version.entry(version_key(version)).or_insert(Redaction { + version: version.clone(), + value: *value, retained: false, }); assert_eq!( - entry.value, value, - "one key logged with two values: content addressing is broken" + entry.value, *value, + "one version logged with two values: a version names exactly \ + one message" ); entry.retained |= retained; } @@ -859,7 +862,7 @@ pub async fn run_plan(plan: Plan) -> SimOutcome { peers: slots.into_iter().flatten().map(Peer::into_rumors).collect(), possible_losses, inserted, - redactions: by_key.into_values().collect(), + redactions: by_version.into_values().collect(), } } @@ -894,20 +897,21 @@ pub async fn quiesce(peers: &[Rumors]) { panic!("heal phase did not converge within {max_rounds} rounds for {n} peers"); } -/// One readout per survivor, in fleet order: the `Key → value` lens -/// every converged-fleet assertion consumes. +/// One readout per survivor, in fleet order: the identity → value lens +/// (keyed by [`version_key`]) every converged-fleet assertion consumes. /// /// Compute once after the heal and thread through [`assert_converged`], /// [`assert_deletion_honored`], and [`assert_value_oracle`]. -pub fn survivor_readouts(peers: &[Rumors]) -> Vec> { +pub fn survivor_readouts(peers: &[Rumors]) -> Vec, u64>> { peers.iter().map(|p| readout(&p.snapshot())).collect() } /// After healing, every survivor holds identical live content: equal -/// `Key → value` readouts, equal observable hashes, equal causal versions. +/// identity → value readouts, equal observable hashes, equal causal +/// versions. /// /// `readouts` is the fleet's [`survivor_readouts`], indexed like `peers`. -pub fn assert_converged(peers: &[Rumors], readouts: &[BTreeMap]) { +pub fn assert_converged(peers: &[Rumors], readouts: &[BTreeMap, u64>]) { assert_eq!(peers.len(), readouts.len(), "one readout per survivor"); let Some(first) = peers.first() else { return }; let snapshot = first.snapshot(); @@ -923,22 +927,23 @@ pub fn assert_converged(peers: &[Rumors], readouts: &[BTreeMap]) } /// Asserts deletion honoring against the execution-time redaction log: no -/// retained redaction's key is live at any survivor. +/// retained redaction's message is live at any survivor. /// /// Unconditional over every retained redaction — and every redaction is /// retained when [`SimOutcome::possible_losses`] is zero. A non-retained /// redaction (its every logger dropped in a retire loss arm) may honestly /// never have reached the survivors, so it is exempt: asserting it would /// fail runs in which the protocol did nothing wrong. -pub fn assert_deletion_honored(readouts: &[BTreeMap], redactions: &[Redaction]) { +pub fn assert_deletion_honored(readouts: &[BTreeMap, u64>], redactions: &[Redaction]) { for redaction in redactions.iter().filter(|r| r.retained) { for (i, live) in readouts.iter().enumerate() { assert!( - !live.contains_key(&redaction.key), - "deletion honoring violated: key {:?} (value {}) was redacted \ - during the run, the redaction is retained in the surviving \ - fleet's history, and yet the key is live at survivor {i}", - redaction.key, + !live.contains_key(redaction.version.as_bytes()), + "deletion honoring violated: version {:?} (value {}) was \ + redacted during the run, the redaction is retained in the \ + surviving fleet's history, and yet the message is live at \ + survivor {i}", + redaction.version, redaction.value, ); } @@ -947,7 +952,7 @@ pub fn assert_deletion_honored(readouts: &[BTreeMap], redactions: &[Re /// Asserts the converged value multiset equals the ledger: every survivor's /// live values are exactly the plan's inserts minus one instance per -/// redacted key. +/// redacted message. /// /// Gated on `possible_losses == 0` (a nonzero count returns without /// checking): zero possible losses covers message content across faulted @@ -960,7 +965,7 @@ pub fn assert_deletion_honored(readouts: &[BTreeMap], redactions: &[Re /// loss-free run conserves every insert and propagates every redaction, /// so the multiset equality is exact. pub fn assert_value_oracle( - readouts: &[BTreeMap], + readouts: &[BTreeMap, u64>], possible_losses: usize, inserted: &[u64], redactions: &[Redaction], @@ -974,14 +979,14 @@ pub fn assert_value_oracle( } for redaction in redactions { // Loss-free runs retain every redaction; each removes exactly one - // instance of its key's value. A redacted value absent from the + // instance of its message's value. A redacted value absent from the // insert ledger is a harness accounting bug, not a protocol bug. match expected.get_mut(&redaction.value) { Some(count) if *count > 0 => *count -= 1, _ => panic!( - "value-ledger accounting bug: redacted key {:?} carried value \ - {}, which the insert ledger does not hold", - redaction.key, redaction.value, + "value-ledger accounting bug: redacted version {:?} carried \ + value {}, which the insert ledger does not hold", + redaction.version, redaction.value, ), } } diff --git a/tests/common/wire.rs b/tests/common/wire.rs index 9c956ca35..3e4df5e99 100644 --- a/tests/common/wire.rs +++ b/tests/common/wire.rs @@ -10,7 +10,6 @@ use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll, Waker}; -use borsh::{BorshDeserialize, BorshSerialize}; use rumors::link::MemoryLink; use rumors::{Peer, Protocol, Rumors, testing::run_to_quiescence}; use tokio::io::{AsyncRead, ReadBuf}; @@ -18,6 +17,8 @@ use tokio::runtime::Runtime; use crate::common::window::WindowChoice; +use serde::Serialize; +use serde::de::DeserializeOwned; // clippy's `missing_const_for_thread_local` misreads `thread_local!`'s // fallback-TLS lowering (illumos among the gate's targets) and denies // initializers that already sit in `const` blocks; the allow keeps @@ -121,7 +122,7 @@ fn unread_control_bytes(mut read: R) -> Vec { #[track_caller] pub fn wire_gossip(a: &Rumors, b: &Rumors) where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { block_on(wire_gossip_async(a, b)); } @@ -130,7 +131,7 @@ where /// block on this thread's runtime (where a nested [`block_on`] would panic). pub async fn wire_gossip_async(a: &Rumors, b: &Rumors) where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { let _ = gossip_pair_async(a, b).await; } @@ -145,7 +146,7 @@ pub async fn gossip_pair_async( b: &Rumors, ) -> (rumors::Gossiped, rumors::Gossiped) where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { let (mut a_link, mut b_link) = rumors::link::memory_with_capacity(LINK_BUF); @@ -206,7 +207,7 @@ pub async fn divergent_pair( #[track_caller] pub fn bootstrap_fork(parent: &Rumors) -> Rumors where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { block_on(bootstrap_fork_async_with_protocol(parent, Protocol::V2)) } @@ -215,7 +216,7 @@ where /// block on this thread's runtime (where a nested [`block_on`] would panic). pub async fn bootstrap_fork_async(parent: &Rumors) -> Rumors where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { bootstrap_fork_async_with_protocol(parent, Protocol::V2).await } @@ -231,7 +232,7 @@ pub async fn bootstrap_fork_async_with_protocol( protocol: Protocol, ) -> Rumors where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { bootstrap_fork_configured(parent, protocol, WindowChoice::Floor).await } @@ -241,7 +242,7 @@ where #[track_caller] pub fn bootstrap_fork_with_window(parent: &Rumors, window: WindowChoice) -> Rumors where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { block_on(bootstrap_fork_with_window_async(parent, window)) } @@ -253,7 +254,7 @@ pub async fn bootstrap_fork_with_window_async( window: WindowChoice, ) -> Rumors where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { bootstrap_fork_configured(parent, Protocol::V2, window).await } @@ -266,7 +267,7 @@ async fn bootstrap_fork_configured( window: WindowChoice, ) -> Rumors where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { let (mut parent_link, mut boot_link) = rumors::link::memory_with_capacity(LINK_BUF); diff --git a/tests/dispute_wire.rs b/tests/dispute_wire.rs index a7ea7dc1a..1ebe3f46f 100644 --- a/tests/dispute_wire.rs +++ b/tests/dispute_wire.rs @@ -14,12 +14,17 @@ //! What the counts establish: the current format's end-to-end cost of one //! disputed message — its question share, reply share, and leaf record — //! is affine in the record's encoded payload, the calibrated intercept -//! plus the payload's borsh encoding. The constant is that cost at the +//! plus the payload's CBOR encoding. The constant is that cost at the //! design point's [`DESIGN_ENCODED_PAYLOAD_BYTES`]-byte record; leaner //! records cost proportionally less wire per message. Three cells pin //! the line — the intercept, an interior point, and the design point — //! so a change to the per-record framing or the record body moves at //! least one loudly, and the linearity claim is itself gated. +//! +//! Payload corpora are [`bytes::Bytes`], which serde carries as a CBOR +//! byte string: a fixed-length payload has one deterministic encoded +//! size (a header plus the raw bytes), which is what lets each cell +//! state its encoded payload size exactly. mod common; @@ -29,7 +34,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::task::{Context, Poll}; -use borsh::{BorshDeserialize, BorshSerialize}; +use bytes::Bytes; use rand::rngs::SmallRng; use rand::{RngCore, SeedableRng}; use rumors::link::{Connector, Done, Link, LinkParts, MemoryLink}; @@ -39,6 +44,8 @@ use tokio::io::AsyncWrite; use crate::common::wire::block_on; +use serde::Serialize; +use serde::de::DeserializeOwned; /// Messages both peers share before the fork: enough that the disputed /// frontier crosses shared structure, as real sessions do. const COMMON: usize = 2_048; @@ -63,21 +70,30 @@ fn fixed_overhead_bytes() -> usize { dispute_overhead_bytes() } -/// The `Vec` payload length whose borsh encoding (a 4-byte length -/// prefix plus the bytes, 172 B) prices a disputed message at exactly +/// The `Bytes` payload length whose CBOR encoding (a 2-byte byte-string +/// header plus the bytes, 172 B) prices a disputed message at exactly /// `DISPUTE_WIRE_BYTES` under the current format. /// /// This is the record size the design-point constant is denominated in. -const DESIGN_PAYLOAD_LEN: usize = 168; +const DESIGN_PAYLOAD_LEN: usize = 170; -/// A mid-size `Vec` payload length (64 B encoded behind borsh's -/// 4-byte length prefix): the interior cell that holds the affine law +/// A mid-size `Bytes` payload length (64 B encoded behind CBOR's 2-byte +/// byte-string header): the interior cell that holds the affine law /// between the minimal and design endpoints. -const MID_PAYLOAD_LEN: usize = 60; +const MID_PAYLOAD_LEN: usize = 62; + +/// CBOR's byte-string header width for lengths in `24..=255`: the major +/// type byte plus one length byte. +const CBOR_BSTR_HEADER_BYTES: usize = 2; /// The design record's encoded payload: [`DESIGN_PAYLOAD_LEN`] bytes -/// behind borsh's 4-byte length prefix. -const DESIGN_ENCODED_PAYLOAD_BYTES: usize = 4 + DESIGN_PAYLOAD_LEN; +/// behind CBOR's byte-string header. +const DESIGN_ENCODED_PAYLOAD_BYTES: usize = CBOR_BSTR_HEADER_BYTES + DESIGN_PAYLOAD_LEN; + +/// A random `u64`'s CBOR encoding: the one-byte major-type header plus +/// eight value bytes (every seeded draw exceeds 2³², so the width is +/// deterministic for the minimal cell's corpus). +const U64_ENCODED_BYTES: usize = 9; /// Slack on each pinned per-message figure, in bytes. The counts are /// deterministic, so the slack only absorbs integer-division adjacency; @@ -167,7 +183,7 @@ fn counting( /// [`DIVERGENT`] minted payloads on each side, deterministically. fn diverged(mut mint: impl FnMut(&mut SmallRng) -> T) -> (Rumors, Rumors) where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { let left = Peer::seed().sync_window_floor().into_rumors(); let mut rng = SmallRng::seed_from_u64(0x0b05_2026_d15b_073e); @@ -189,7 +205,7 @@ where /// side of each end. fn session_wire_bytes(a: &Rumors, b: &Rumors) -> usize where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { let written = Arc::new(AtomicUsize::new(0)); let (a_link, b_link) = rumors::link::memory_with_capacity(LINK_CAPACITY); @@ -212,7 +228,7 @@ where /// cost the constant states. fn implied_bytes_per_message(mint: impl FnMut(&mut SmallRng) -> T) -> usize where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { let (left, right) = diverged(mint); let total = session_wire_bytes(&left, &right); @@ -240,9 +256,9 @@ fn dispute_wire_bytes_is_the_design_record_cost() { let mut mint = |rng: &mut SmallRng| { let mut payload = vec![0u8; DESIGN_PAYLOAD_LEN]; rng.fill_bytes(&mut payload); - payload + Bytes::from(payload) }; - let implied = implied_bytes_per_message::>(&mut mint); + let implied = implied_bytes_per_message::(&mut mint); let (_, constant) = envelope_and_wire_bytes(); eprintln!( "design-record cell: implied {implied} B/message at {DESIGN_ENCODED_PAYLOAD_BYTES} B \ @@ -260,23 +276,23 @@ fn dispute_wire_bytes_is_the_design_record_cost() { /// record framing beyond the encoded payload — is pinned at the /// minimal-payload end of the line. /// -/// The invariant: a `u64` corpus (8 B encoded payloads) implies -/// the calibrated intercept + 8 bytes per disputed message. Together -/// with the design-record cell this pins both parameters of the affine -/// cost `overhead + encoded_payload`, so framing drift cannot hide -/// inside the design cell's payload term. It is also the honest floor: -/// minimal-payload sessions cost ~42 B of wire per disputed message, -/// several times less than the design constant, and correspondingly -/// need more scopes in flight to fill the same link. +/// The invariant: a `u64` corpus ([`U64_ENCODED_BYTES`]-byte encoded +/// payloads) implies the calibrated intercept + that width per disputed +/// message. Together with the design-record cell this pins both +/// parameters of the affine cost `overhead + encoded_payload`, so +/// framing drift cannot hide inside the design cell's payload term. It +/// is also the honest floor: minimal-payload sessions cost several +/// times less wire per disputed message than the design constant, and +/// correspondingly need more scopes in flight to fill the same link. #[test] fn minimal_records_pin_the_fixed_overhead() { let implied = implied_bytes_per_message::(|rng| rng.next_u64()); - let expected = fixed_overhead_bytes() + std::mem::size_of::(); + let expected = fixed_overhead_bytes() + U64_ENCODED_BYTES; eprintln!("minimal-record cell: implied {implied} B/message (expected {expected})"); assert!( expected.abs_diff(implied) <= TOLERANCE_BYTES, - "the fixed per-message overhead moved: measured {implied} B at 8 B encoded \ - payloads against the pinned {expected} B", + "the fixed per-message overhead moved: measured {implied} B at \ + {U64_ENCODED_BYTES} B encoded payloads against the pinned {expected} B", ); } @@ -295,16 +311,16 @@ fn mid_size_records_ride_the_affine_law() { let mut mint = |rng: &mut SmallRng| { let mut payload = vec![0u8; MID_PAYLOAD_LEN]; rng.fill_bytes(&mut payload); - payload + Bytes::from(payload) }; - let implied = implied_bytes_per_message::>(&mut mint); - let expected = fixed_overhead_bytes() + 4 + MID_PAYLOAD_LEN; + let implied = implied_bytes_per_message::(&mut mint); + let expected = fixed_overhead_bytes() + CBOR_BSTR_HEADER_BYTES + MID_PAYLOAD_LEN; eprintln!("mid-record cell: implied {implied} B/message (expected {expected})"); assert!( expected.abs_diff(implied) <= TOLERANCE_BYTES, "the affine cost law broke in the interior: measured {implied} B at \ {} B encoded payloads against the pinned {expected} B", - 4 + MID_PAYLOAD_LEN, + CBOR_BSTR_HEADER_BYTES + MID_PAYLOAD_LEN, ); } diff --git a/tests/disruption.rs b/tests/disruption.rs index cac4fb3d0..834ed2b20 100644 --- a/tests/disruption.rs +++ b/tests/disruption.rs @@ -61,7 +61,7 @@ proptest! { /// 4. when no hand-off was lost in flight, the surviving parties /// fold-join back to exactly `Party::seed()` — the id-space is /// conserved with no duplication and no leak; - /// 5. no retained redaction's key is live at any survivor (deletion + /// 5. no retained redaction's message is live at any survivor (deletion /// honoring against the execution-time redaction log; every /// redaction is retained whenever `possible_losses` is zero); /// 6. when no hand-off was lost in flight, the converged value @@ -132,7 +132,7 @@ fn panics(f: impl FnOnce()) -> bool { /// A small, fault-free plan for the tripwires. /// /// Deterministically loss-free by construction (no fault is ever -/// injected), with enough content that a live key always exists to +/// injected), with enough content that a live message always exists to /// corrupt the ledger around. fn tripwire_plan() -> Plan { Plan { @@ -162,7 +162,7 @@ fn tripwire_plan() -> Plan { /// A *suppressed redaction* — the application called `redact()` (so the /// ledger holds it) but the mechanism left the leaf live — must fail both /// the deletion-honoring check and the multiset check; it is simulated by -/// appending a ledger entry for a key that is genuinely live in the +/// appending a ledger entry for a message that is genuinely live in the /// converged fleet. A *dropped insert* — a value the plan sent but the /// network silently lost — must fail the multiset check; it is simulated /// by appending a never-sent value to the insert ledger. The uncorrupted @@ -185,14 +185,16 @@ fn value_oracle_tripwires_catch_known_bad_mechanisms() { assert_value_oracle(&readouts, 0, &outcome.inserted, &outcome.redactions); // Known-bad mechanism 1: a suppressed redaction. Its ledger entry - // names a key still live in the converged fleet. - let (&key, &value) = readouts[0] + // names a message still live in the converged fleet. (Readout keys + // are canonical version bytes, so the version decodes back out.) + let (key, &value) = readouts[0] .iter() .next() .expect("the tripwire plan leaves live content"); let mut suppressed = outcome.redactions.clone(); suppressed.push(Redaction { - key, + version: rumors::Version::decode(key.as_slice()) + .expect("readout keys are canonical version bytes"), value, retained: true, }); @@ -324,14 +326,15 @@ fn value_oracle_survives_committed_retire_chain() { assert_value_oracle(&readouts, 0, &outcome.inserted, &outcome.redactions); // Liveness after the custody weakening: fabricating a retained - // redaction of a live key must still fire both checks. - let (&key, &value) = readouts[0] + // redaction of a live message must still fire both checks. + let (key, &value) = readouts[0] .iter() .next() .expect("live content survives the chain"); let mut corrupted = outcome.redactions.clone(); corrupted.push(Redaction { - key, + version: rumors::Version::decode(key.as_slice()) + .expect("readout keys are canonical version bytes"), value, retained: true, }); @@ -388,12 +391,15 @@ async fn envelope_session_bytes() -> usize { // that would blunt the divergence. for (i, peer) in fleet.iter().enumerate() { peer.send(2_000_000 + i as u64); - let (marker, _, _) = peer - .snapshot() - .iter() - .next() - .expect("the peer holds exactly its own marker"); - peer.redact(marker); + let marker = { + let snapshot = peer.snapshot(); + let (marker, _) = snapshot + .iter() + .next() + .expect("the peer holds exactly its own marker"); + marker.clone() + }; + peer.redact(&marker); } // Two star rounds spread every party's ticks into every peer's // bounds (the first collects at the hub, the second redistributes). diff --git a/tests/gossip_snapshot.rs b/tests/gossip_snapshot.rs index 8f5908b93..a0422dbb7 100644 --- a/tests/gossip_snapshot.rs +++ b/tests/gossip_snapshot.rs @@ -10,9 +10,9 @@ //! never as an accommodation of drift; the two-regime re-accept rule and //! its procedure (`cargo insta review`) are in `AGENTS.md`. //! -//! The payload type is `u64` throughout: it borsh-encodes to a fixed 8 bytes -//! and is trivial to make distinct, which keeps the dumps short and lets -//! distinct payloads (`1`, `2`, `3`, `4`) be spotted directly in the hex. +//! The payload type is `u64` throughout: a small integer is one CBOR byte +//! (`01`, `02`, …), which keeps the dumps short and lets distinct payloads +//! be spotted directly in the hex. mod common; @@ -20,11 +20,14 @@ use rand::SeedableRng; use rand::rngs::SmallRng; #[cfg(feature = "protocol-v1")] use rumors::Protocol; -use rumors::{Key, Peer, Rumors}; +use rumors::{Peer, Rumors, Version}; use crate::common::gossip_snapshot::capture_gossip; #[cfg(feature = "protocol-v1")] use crate::common::gossip_snapshot::capture_gossip_v1; +use crate::common::shape::{ + ballast_avoiding, keep_only, leaf_path, path_radix, pool, send_pool, shaped_pair, +}; #[cfg(feature = "protocol-v1")] use crate::common::wire::bootstrap_fork_async_with_protocol; use crate::common::wire::{block_on, bootstrap_fork, bootstrap_fork_async}; @@ -38,14 +41,14 @@ fn seeded() -> Rumors { .into_rumors() } -/// The key of the live message holding `value`: how a scenario picks out a -/// specific message for redaction. Keys are content-addressed and the -/// scenarios use distinct payloads, so the lookup is unambiguous. -fn key_for(rumors: &Rumors, value: u64) -> Key { +/// The version of the live message holding `value`: how a scenario picks +/// out a specific message for redaction. The scenarios use distinct +/// payloads, so the lookup is unambiguous. +fn version_for(rumors: &Rumors, value: u64) -> Version { rumors .snapshot() .iter() - .find_map(|(k, _, m)| (**m == value).then_some(k)) + .find_map(|(v, m)| (**m == value).then_some(v.clone())) .unwrap_or_else(|| panic!("no live message holds {value}")) } @@ -83,45 +86,59 @@ fn one_sided_transfer() { insta::assert_snapshot!(capture_gossip(a, b)); } -/// Values whose two messages, batch-sent in this order into the seeded -/// universe of [`batched_supply_run`], produce keys sharing their first two -/// bytes (`71 06`; found by search over the second value). -/// -/// The populated -/// responder ships its root children as whole height-31 supplies, so the -/// shared leading byte places both leaves inside one supplied subtree (the -/// two-byte collision is stronger than that supply needs, and keeps the -/// pair inside one subtree at height 30 as well). -const COLLIDING_VALUES: (u64, u64) = (1, 27730); +/// Pool size for [`colliding_pair`]'s two-byte search: paths are uniform, +/// so a two-byte agreement needs a birthday-scale pool over 2¹⁶. +const COLLIDING_POOL: u64 = 1024; -/// One supplied subtree holding two leaves pins a batched run on the wire. +/// Stage the batched-run universe: a populated peer holding exactly two +/// leaves whose paths share their first two bytes, against an empty fork. /// -/// Every other fixture supplies single-leaf subtrees, so no other snapshot -/// contains a multi-record run body. Here the transfer's two keys share a -/// two-byte prefix, so the populated peer ships them as a single Supply -/// frame whose run carries two length-prefixed records back to back — the -/// byte-for-byte pin of the batched wire form. -#[test] -fn batched_supply_run() { +/// Paths are version-derived, so the shape is staged by minting a pool of +/// sends, searching the minted versions for the first two-byte agreement, +/// and redacting the rest — deterministic under the seeded universe (see +/// `common::shape`). The shared leading byte places both leaves inside one +/// supplied root child (the two-byte agreement is stronger than that +/// supply needs, and keeps the pair inside one subtree at height 30 as +/// well). +fn colliding_pair() -> (Rumors, Rumors) { let (a, b) = block_on(async { let a: Rumors = seeded(); let b = bootstrap_fork_async(&a).await; - let (first, second) = COLLIDING_VALUES; - a.batch().send(first).send(second); + send_pool(&a, 0, COLLIDING_POOL); (a, b) }); - // Self-check the fixture: if hashing or version assignment drifts, fail - // here with a clear message rather than in the snapshot hex. + let (first, second) = shaped_pair(&pool(&a, 0, COLLIDING_POOL), 2, false); + keep_only(&a, 0, COLLIDING_POOL, &[first, second]); + + // Self-check the landed shape: if hashing or version assignment + // drifts, fail here with a clear message rather than in the hex. let prefixes: Vec<[u8; 2]> = a .snapshot() .iter() - .map(|(k, _, _)| [k.as_bytes()[0], k.as_bytes()[1]]) + .map(|(v, _)| { + let path = leaf_path(v); + [path[0], path[1]] + }) .collect(); + assert_eq!(prefixes.len(), 2, "the fixture holds exactly the pair"); assert_eq!( prefixes.first(), prefixes.last(), - "the fixture's two keys must share a two-byte prefix to share a supplied subtree" + "the fixture's two leaf paths must share a two-byte prefix to share a supplied subtree" ); + (a, b) +} + +/// One supplied subtree holding two leaves pins a batched run on the wire. +/// +/// Every other fixture supplies single-leaf subtrees, so no other snapshot +/// contains a multi-record run body. Here the transfer's two leaf paths +/// share a two-byte prefix, so the populated peer ships them as a single +/// Supply frame whose run carries two length-prefixed records back to back +/// — the byte-for-byte pin of the batched wire form. +#[test] +fn batched_supply_run() { + let (a, b) = colliding_pair(); insta::assert_snapshot!(capture_gossip(a, b)); } @@ -136,18 +153,13 @@ fn batched_supply_run() { /// ran at the minimum of the two settings. #[test] fn asymmetric_message_targets_unbatch_the_run() { - let (a, b) = block_on(async { - let a: Rumors = seeded(); - let b = bootstrap_fork_async(&a).await; - let (first, second) = COLLIDING_VALUES; - a.batch().send(first).send(second); - let b = b - .try_into_peer() + let (a, b) = colliding_pair(); + let b = block_on(async { + b.try_into_peer() .await .expect("the bootstrapped handle is sole") .target_message_size(0) - .into_rumors(); - (a, b) + .into_rumors() }); insta::assert_snapshot!(capture_gossip(a, b)); } @@ -178,67 +190,72 @@ fn stream_frames(capture: &str, header: &str) -> Option> { frames } -/// Values whose two messages, batch-sent in this order into the seeded -/// universe of [`bulk_initiator_ships_opening_supplies`], produce keys -/// `71 06` and `71 67` (found by search over the second value). -/// -/// A shared -/// first byte and distinct second bytes, so the initiator's one exclusive -/// root child holds a two-leaf subtree whose leaves split one level down. -const INITIATOR_SUBTREE_VALUES: (u64, u64) = (1, 287); +/// Pool size for a one-byte *pair* search (any two paths agreeing on +/// their root radix): a birthday search over 256 radixes, hit early. +const RADIX_POOL: u64 = 64; -/// First of three consecutive ballast values for the responder of -/// [`bulk_initiator_ships_opening_supplies`]. -/// -/// Their keys' first bytes -/// (`1e`, `6a`, `f6`) avoid the initiator's exclusive radix (`71`), and the -/// extra message makes the responder the larger set, so the subtree holder -/// wins the initiator election. -const RESPONDER_BALLAST_FROM: u64 = 100; +/// Pool size for hitting one *specific* root radix: a direct-hit search +/// with mean 256, sized well past it. +const TARGETED_POOL: u64 = 2048; + +/// Payload base and pool size for a fixture's responder ballast: a +/// disjoint payload range so pool cleanups never touch the other side's +/// messages. +const BALLAST_POOL: (u64, u64) = (10_000, 16); /// A bulk-holding initiator ships its exclusive root children whole at the /// opening, on its own stream 0, without waiting for the responder's empty /// queries. /// /// The initiator (the smaller set) holds one exclusive root child with two -/// leaves splitting at the second key byte. The pinned shape is the +/// leaves splitting at the second path byte. The pinned shape is the /// supply-only opening: the whole child crosses as a single two-record /// Supply run on `Initiator stream 0 (height 31)`, and the responder's /// root-level empty query is answered by a bare empty reply at height 30 /// instead of one decomposed Supply frame per second-byte child. #[test] fn bulk_initiator_ships_opening_supplies() { + // Stage: the initiator holds exactly two leaves sharing a root radix + // and splitting one level down (pool-search-and-redact; the shape is a + // function of the minted versions, see `common::shape`); the responder + // holds three ballast leaves outside that radix, making it the larger + // set so the subtree holder initiates. let (a, b) = block_on(async { let a: Rumors = seeded(); let b = bootstrap_fork_async(&a).await; - let (first, second) = INITIATOR_SUBTREE_VALUES; - a.batch().send(first).send(second); - let y = RESPONDER_BALLAST_FROM; - b.batch().send(y).send(y + 1).send(y + 2); + send_pool(&a, 0, RADIX_POOL); (a, b) }); + let (first, second) = shaped_pair(&pool(&a, 0, RADIX_POOL), 1, true); + keep_only(&a, 0, RADIX_POOL, &[first, second]); + let radix = path_radix(&version_for(&a, first)); + let (ballast_from, ballast_pool) = BALLAST_POOL; + send_pool(&b, ballast_from, ballast_pool); + let ballast = ballast_avoiding(&pool(&b, ballast_from, ballast_pool), radix, 3); + keep_only(&b, ballast_from, ballast_pool, &ballast); // Fixture self-checks: the initiator-exclusive subtree and the election. - let akeys: Vec<[u8; 2]> = a + let apaths: Vec<[u8; 2]> = a .snapshot() .iter() - .map(|(k, _, _)| [k.as_bytes()[0], k.as_bytes()[1]]) + .map(|(v, _)| { + let path = leaf_path(v); + [path[0], path[1]] + }) .collect(); assert_eq!( - akeys.first().map(|k| k[0]), - akeys.last().map(|k| k[0]), - "the initiator's two keys must share a root radix" + apaths.first().map(|p| p[0]), + apaths.last().map(|p| p[0]), + "the initiator's two leaf paths must share a root radix" ); assert_ne!( - akeys.first().map(|k| k[1]), - akeys.last().map(|k| k[1]), - "the initiator's two keys must split one level below the root" + apaths.first().map(|p| p[1]), + apaths.last().map(|p| p[1]), + "the initiator's two leaf paths must split one level below the root" ); - let radix = akeys[0][0]; + let radix = apaths[0][0]; assert!( - b.snapshot() - .iter() - .all(|(k, _, _)| k.as_bytes()[0] != radix), + b.snapshot().iter().all(|(v, _)| leaf_path(v)[0] != radix), "the responder must lack the initiator's exclusive radix" ); assert!( @@ -265,17 +282,6 @@ fn bulk_initiator_ships_opening_supplies() { insta::assert_snapshot!(capture); } -/// Values for [`early_supplies_honor_redactions`]: the second, sent after -/// the responder forks, lands its key under the same root radix as the -/// first's (keys `09 a7` and `09 5a`), found by search. -const REDACTION_SUBTREE_VALUE: u64 = 165; - -/// First of three consecutive ballast values for the responder of -/// [`early_supplies_honor_redactions`]: their keys' first bytes (`94`, -/// `cf`, `e9`) avoid the shared radix (`09`), and they make the responder -/// the larger set. -const REDACTION_BALLAST_FROM: u64 = 100; - /// Deletion honoring prunes the opening supplies: a redacted message does /// not resurrect through the early path, and the supply carries the /// survivor rather than the full subtree. @@ -289,35 +295,43 @@ const REDACTION_BALLAST_FROM: u64 = 100; /// pinned bytes show one. #[test] fn early_supplies_honor_redactions() { + // Stage: the initiator's first message exists before the fork (so the + // responder once held it), and a pool search lands a second initiator + // leaf under the same root radix; the responder redacts its copy of + // the first and keeps three ballast leaves outside that radix, making + // it the larger set. let (a, b) = block_on(async { let a: Rumors = seeded(); a.send(1); let b = bootstrap_fork_async(&a).await; - a.send(REDACTION_SUBTREE_VALUE); - b.redact(key_for(&b, 1)); - let y = REDACTION_BALLAST_FROM; - b.batch().send(y).send(y + 1).send(y + 2); + b.redact(&version_for(&b, 1)); (a, b) }); + let radix = path_radix(&version_for(&a, 1)); + send_pool(&a, 2, TARGETED_POOL); + let sibling = pool(&a, 2, TARGETED_POOL) + .into_iter() + .find(|(_, v)| path_radix(v) == radix) + .map(|(value, _)| value) + .expect("some pool leaf lands under the first message's radix"); + keep_only(&a, 2, TARGETED_POOL, &[1, sibling]); + let (ballast_from, ballast_pool) = BALLAST_POOL; + send_pool(&b, ballast_from, ballast_pool); + let ballast = ballast_avoiding(&pool(&b, ballast_from, ballast_pool), radix, 3); + keep_only(&b, ballast_from, ballast_pool, &ballast); // Fixture self-checks: shared radix, cover of the redacted message, // and the election. - let akeys: Vec = a - .snapshot() - .iter() - .map(|(k, _, _)| k.as_bytes()[0]) - .collect(); - assert_eq!(akeys.len(), 2, "the initiator holds the pair"); + let apaths: Vec = a.snapshot().iter().map(|(v, _)| leaf_path(v)[0]).collect(); + assert_eq!(apaths.len(), 2, "the initiator holds the pair"); assert_eq!( - akeys.first(), - akeys.last(), - "both initiator keys must share a root radix" + apaths.first(), + apaths.last(), + "both initiator leaf paths must share a root radix" ); - let radix = akeys[0]; + let radix = apaths[0]; assert!( - b.snapshot() - .iter() - .all(|(k, _, _)| k.as_bytes()[0] != radix), + b.snapshot().iter().all(|(v, _)| leaf_path(v)[0] != radix), "the responder must lack the shared radix outright: it redacted \ its copy" ); @@ -335,23 +349,19 @@ fn early_supplies_honor_redactions() { "one pruned Supply run: the survivor, not the full subtree" ); assert!( - !a.snapshot().iter().any(|(_, _, m)| **m == 1), + !a.snapshot().iter().any(|(_, m)| **m == 1), "the redaction is contagious: the initiator drops the message" ); assert!( - !b.snapshot().iter().any(|(_, _, m)| **m == 1), + !b.snapshot().iter().any(|(_, m)| **m == 1), "the redacted message must not resurrect at the responder" ); assert!( - a.snapshot() - .iter() - .any(|(_, _, m)| **m == REDACTION_SUBTREE_VALUE), + a.snapshot().iter().any(|(_, m)| **m == sibling), "the survivor converges to the initiator" ); assert!( - b.snapshot() - .iter() - .any(|(_, _, m)| **m == REDACTION_SUBTREE_VALUE), + b.snapshot().iter().any(|(_, m)| **m == sibling), "the survivor converges to the responder" ); insta::assert_snapshot!(capture); @@ -397,7 +407,7 @@ fn fork_insert_redact() { a.batch().send(1).send(2); // (2) Fork: B is a genuine disjoint fork sharing A's observations - // (both hold 1 and 2, under the same keys). + // (both hold 1 and 2, under the same versions). let b = bootstrap_fork_async(&a).await; // (3) Each fork inserts one distinct message. @@ -405,8 +415,8 @@ fn fork_insert_redact() { b.send(4); // (4) Each fork redacts a different one of the two common messages. - a.redact(key_for(&a, 1)); - b.redact(key_for(&b, 2)); + a.redact(&version_for(&a, 1)); + b.redact(&version_for(&b, 2)); (a, b) }); @@ -444,7 +454,7 @@ fn redaction_only() { let a: Rumors = seeded(); a.batch().send(1).send(2); let b = bootstrap_fork_async(&a).await; - a.redact(key_for(&a, 1)); + a.redact(&version_for(&a, 1)); (a, b) }); insta::assert_snapshot!(capture_gossip(a, b)); @@ -490,11 +500,10 @@ fn deep_trie_divergence() { /// A non-primitive, variable-length payload type. /// -/// `u64` borsh-encodes to a -/// fixed 8 bytes; `String` encodes as a length prefix followed by its UTF-8 -/// bytes, so this is the only scenario that pins how a variable-length value -/// is framed inside a leaf on the wire. `A` and `B` each contribute one -/// distinct string and converge on both. +/// A `String` encodes as a CBOR text string — a header byte carrying the +/// length, then the UTF-8 bytes — so this is the scenario that pins how a +/// variable-length value is framed inside a leaf on the wire. `A` and `B` +/// each contribute one distinct string and converge on both. #[test] fn string_payload() { let (a, b) = block_on(async { @@ -526,7 +535,7 @@ fn same_live_content_divergent_versions() { // A diverges in version but not in live content: insert 2, then drop it. a.send(2); - a.redact(key_for(&a, 2)); + a.redact(&version_for(&a, 2)); (a, b) }); insta::assert_snapshot!(capture_gossip(a, b)); @@ -535,20 +544,21 @@ fn same_live_content_divergent_versions() { /// Concurrent, identical redaction. /// /// Both forks hold `1` and `2`, and *each* -/// independently redacts `1` (the same [`Key`]) before they gossip. The two -/// redactions are causally concurrent — distinct version advances on distinct -/// parties — yet target the same message, so this pins that the protocol -/// converges idempotently on `{2}` rather than treating the two redactions as +/// independently redacts `1` (the same message: a bootstrap copies the +/// leaf, [`Version`] included) before they gossip. The two redactions are +/// causally concurrent — distinct version advances on distinct parties — +/// yet target the same message, so this pins that the protocol converges +/// idempotently on `{2}` rather than treating the two redactions as /// conflicting work to reconcile. #[test] -fn both_redact_same_key() { +fn both_redact_the_same_message() { let (a, b) = block_on(async { let a: Rumors = seeded(); a.batch().send(1).send(2); let b = bootstrap_fork_async(&a).await; - let k1 = key_for(&a, 1); - a.redact(k1); - b.redact(k1); + let v1 = version_for(&a, 1); + a.redact(&v1); + b.redact(&v1); (a, b) }); insta::assert_snapshot!(capture_gossip(a, b)); diff --git a/tests/gossip_when.rs b/tests/gossip_when.rs index 3fa570465..62689f566 100644 --- a/tests/gossip_when.rs +++ b/tests/gossip_when.rs @@ -42,7 +42,7 @@ use rumors::{Error, Gossiped, Led, Peer, Rumors, testing::run_to_quiescence}; use tokio::io::AsyncWriteExt; use tokio::time::timeout; -use crate::common::action::minted_key; +use crate::common::action::minted_version; use crate::common::fault::{FaultPlan, faulty}; use crate::common::wire::{bootstrap_fork_async, tokio_block_on as block_on, wire_gossip_async}; @@ -273,7 +273,7 @@ async fn changes_propagate_transitively_through_a_chain() { loop { c_changes.next().await.expect("set still open"); let snapshot = c.snapshot(); - if snapshot.iter().any(|(_, _, m)| **m == 42) { + if snapshot.iter().any(|(_, m)| **m == 42) { return; } } @@ -338,7 +338,7 @@ async fn a_redaction_frontier_propagates_transitively_through_a_chain() { // trace is A's advanced causal frontier. let pre = a.snapshot().latest().clone(); a.send(42); - a.redact(minted_key(&a.snapshot(), &pre)); + a.redact(&minted_version(&a.snapshot(), &pre)); // The A-side connection carries the news to B... let ab = futures::future::join(a_drv.next(), b_ab_drv.next()); @@ -682,8 +682,8 @@ proptest! { // Atomicity: whatever happened, each side holds its own send, // nothing beyond the union, and never a torn intermediate. let (a_snapshot, b_snapshot) = (a.snapshot(), b.snapshot()); - assert!(a_snapshot.iter().any(|(_, _, m)| **m == 1)); - assert!(b_snapshot.iter().any(|(_, _, m)| **m == 2)); + assert!(a_snapshot.iter().any(|(_, m)| **m == 1)); + assert!(b_snapshot.iter().any(|(_, m)| **m == 2)); assert!(a_snapshot.len() <= 2); assert!(b_snapshot.len() <= 2); diff --git a/tests/handshake_liveness.rs b/tests/handshake_liveness.rs index d5f6ac0a9..7bcb46c4a 100644 --- a/tests/handshake_liveness.rs +++ b/tests/handshake_liveness.rs @@ -354,8 +354,8 @@ async fn retire_into_bootstrapper(protocol: Protocol) { let after = successor.snapshot(); assert_eq!(after.len(), before.len(), "no content is lost in handoff"); assert_eq!( - after.iter().map(|(k, _, _)| k).collect::>(), - before.iter().map(|(k, _, _)| k).collect::>(), + after.iter().collect::>(), + before.iter().collect::>(), "the successor holds exactly the retiree's content" ); assert_control_drained(r_link, n_link); diff --git a/tests/hop_trace.rs b/tests/hop_trace.rs index 38b50d4a6..6b498541d 100644 --- a/tests/hop_trace.rs +++ b/tests/hop_trace.rs @@ -40,13 +40,15 @@ use rand::rngs::SmallRng; use rand::seq::SliceRandom; use rand::{RngCore, SeedableRng}; use rumors::link::{Acceptor, Connector, Done, Link, STREAM_COUNT}; -use rumors::{DEFAULT_SYNC_MEMORY_BUDGET, Key, Peer, Protocol, Rumors}; +use rumors::{DEFAULT_SYNC_MEMORY_BUDGET, Peer, Protocol, Rumors, Version}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use tokio::sync::mpsc; use tokio::time::Instant; use latency::{DelayedReader, DelayedWriter, delayed_pipe}; +use serde::Serialize; +use serde::de::DeserializeOwned; /// One-way link delay; whole milliseconds per the timer wheel's grain. const DELAY: Duration = Duration::from_millis(10); @@ -327,7 +329,7 @@ fn traced_pair(trace: &Trace) -> (TracedLink, TracedLink) { /// Gossip one pair over a traced delayed link and return the trace. fn traced_session(a: Rumors, b: Rumors) -> Trace where - T: borsh::BorshSerialize + borsh::BorshDeserialize + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { let trace = Trace::default(); let (mut a_link, mut b_link) = traced_pair(&trace); @@ -461,8 +463,8 @@ fn diverged_redactions() -> (Rumors, Rumors) { send_random(&left, COMMON, &mut rng); let right = bootstrap_fork(&left); - let keys: Vec = left.snapshot().iter().map(|(k, _, _)| k).collect(); - let mut shuffled = keys; + let versions: Vec = left.snapshot().iter().map(|(v, _)| v.clone()).collect(); + let mut shuffled = versions; shuffled.shuffle(&mut SmallRng::seed_from_u64(0x84f6_7932_1265_9eec)); redact_all(&left, &shuffled[..REDACT_PER_SIDE]); redact_all(&right, &shuffled[REDACT_PER_SIDE..2 * REDACT_PER_SIDE]); @@ -476,10 +478,10 @@ fn send_random(rumors: &Rumors, count: usize, rng: &mut SmallRng) { } } -fn redact_all(rumors: &Rumors, keys: &[Key]) { +fn redact_all(rumors: &Rumors, versions: &[Version]) { let mut batch = rumors.batch(); - for key in keys { - batch.redact(*key); + for version in versions { + batch.redact(version); } } @@ -528,31 +530,88 @@ fn trace_redaction_session() { /// Two peers with disjoint exclusive content and no dispute, the smaller /// side holding a two-leaf subtree under one root radix. /// -/// The staging reuses `gossip_snapshot.rs`'s searched values: the -/// initiator's two keys share first byte `71` and its ballast counterpart's -/// three keys land at `1e`, `6a`, and `f6`, so no root child is populated -/// on both sides and the session is pure transfer in both directions. +/// The staging requires the initiator's two leaf paths to share their +/// first byte while its ballast counterpart's three land under other root +/// radices, so no root child is populated on both sides and the session is +/// pure transfer in both directions. A leaf's path is the BLAKE3 hash of +/// its version's canonical bytes, so the shape is a property of the minted +/// version sequence; the self-checks below verify it. fn transfer_pair() -> (Rumors, Rumors) { + // Stage by pool search: paths are version-derived, so the shape is a + // deterministic function of the seeded universe and send order — mint + // a pool, pick the versions whose paths land the shape, redact the + // rest. The left peer keeps exactly two leaves sharing a root radix; + // the right keeps three ballast leaves outside it and advertises the + // larger set. + let path_radix = |version: &Version| blake3::hash(version.as_bytes()).as_bytes()[0]; + let keep_only = |rumors: &Rumors, keep: &[u64]| { + let losers: Vec = rumors + .snapshot() + .iter() + .filter(|(_, m)| !keep.contains(m)) + .map(|(v, _)| v.clone()) + .collect(); + let mut batch = rumors.batch(); + for version in &losers { + batch.redact(version); + } + }; + let left = Peer::seed_rng(&mut SmallRng::seed_from_u64(0)) .sync_memory_budget(DEFAULT_SYNC_MEMORY_BUDGET) .into_rumors(); let right = bootstrap_fork(&left); - left.batch().send(1).send(287); - right.batch().send(100).send(101).send(102); - - // Fixture self-checks: mirror the searched shape so drift in hashing or - // version assignment fails here, not in the hop arithmetic. - let radices: Vec = left + { + let mut batch = left.batch(); + for value in 0..64u64 { + batch.send(value); + } + } + let mut pool: Vec<(u64, u8)> = left .snapshot() .iter() - .map(|(k, _, _)| k.as_bytes()[0]) + .map(|(v, m)| (**m, path_radix(v))) .collect(); + pool.sort_unstable(); + let (first, second) = pool + .iter() + .find_map(|&(value, radix)| { + pool.iter() + .find(|&&(other, r)| other > value && r == radix) + .map(|&(other, _)| (value, other)) + }) + .expect("some pool pair shares a root radix"); + keep_only(&left, &[first, second]); + let radix = pool + .iter() + .find_map(|&(value, radix)| (value == first).then_some(radix)) + .expect("the kept pair is in the pool"); + { + let mut batch = right.batch(); + for value in 10_000..10_016u64 { + batch.send(value); + } + } + let ballast: Vec = right + .snapshot() + .iter() + .filter(|(v, _)| path_radix(v) != radix) + .take(3) + .map(|(_, m)| **m) + .collect(); + assert_eq!(ballast.len(), 3, "the ballast pool cannot fill its quota"); + keep_only(&right, &ballast); + + // Fixture self-checks: mirror the required shape so drift in hashing + // or version assignment fails here, not in the hop arithmetic. + let radices: Vec = left.snapshot().iter().map(|(v, _)| path_radix(v)).collect(); + assert_eq!(radices.len(), 2, "the left peer holds exactly the pair"); assert_eq!(radices.first(), radices.last(), "one exclusive subtree"); assert!( right .snapshot() .iter() - .all(|(k, _, _)| k.as_bytes()[0] != radices[0]), + .all(|(v, _)| path_radix(v) != radices[0]), "no root child is populated on both sides" ); assert!( diff --git a/tests/lifecycle.rs b/tests/lifecycle.rs index b7dd6caf4..6840c29a3 100644 --- a/tests/lifecycle.rs +++ b/tests/lifecycle.rs @@ -44,7 +44,7 @@ const MID_FLIGHT_POLLS: usize = 4; /// /// Construction is deterministic apart from the random network id (which /// has a fixed wire length): versions derive from the bootstrap order and -/// message keys from `(version, payload)`, so two calls build pairs whose +/// message identity from the version alone, so two calls build pairs whose /// gossip sessions are byte-for-byte the same size. The epilogue-residue /// test's measure-then-replay rests on this. async fn divergent_pair() -> (Rumors, Rumors) { diff --git a/tests/listen.rs b/tests/listen.rs index 312084415..9fc88636a 100644 --- a/tests/listen.rs +++ b/tests/listen.rs @@ -20,16 +20,16 @@ use proptest::collection::vec; use proptest::prelude::*; use rand::SeedableRng; use rand::rngs::SmallRng; -use rumors::{Key, Peer, Retire, Rumors, UnorderedMessages, Version, causally}; +use rumors::{Peer, Retire, Rumors, UnorderedMessages, Version, causally}; -use crate::common::action::minted_key; +use crate::common::action::minted_version; use crate::common::wire::{assert_control_drained, block_on, bootstrap_fork, wire_gossip}; /// One observer step, with the borrowed faces cloned out. #[derive(Debug, PartialEq)] enum Step { /// The observer yielded a message. - Item((Key, Version, u64)), + Item((Version, u64)), /// The observer is quiet: nothing new, actors still live. Quiet, /// The observer ended: every sender is gone and the complete final @@ -42,13 +42,13 @@ fn step(obs: &mut UnorderedMessages) -> Step { match obs.borrow_next().now_or_never() { None => Step::Quiet, Some(None) => Step::Ended, - Some(Some((k, v, m))) => Step::Item((k, v.clone(), **m)), + Some(Some((v, m))) => Step::Item((v.clone(), **m)), } } /// Drain the observer until it goes quiet or ends, returning the items in /// delivery order and whether it ended. -fn drain(obs: &mut UnorderedMessages) -> (Vec<(Key, Version, u64)>, bool) { +fn drain(obs: &mut UnorderedMessages) -> (Vec<(Version, u64)>, bool) { let mut items = Vec::new(); loop { match step(obs) { @@ -59,11 +59,15 @@ fn drain(obs: &mut UnorderedMessages) -> (Vec<(Key, Version, u64)>, bool) { } } -/// The live `Key → value` map, for comparing against deliveries. (Keys -/// identify messages uniquely; `Version` is only partially ordered, so it -/// can't key a comparison set.) -fn live_map(rumors: &Rumors) -> BTreeMap { - rumors.snapshot().iter().map(|(k, _, m)| (k, **m)).collect() +/// The live identity → value map, for comparing against deliveries. +/// (A message's identity is its `Version`, which is only partially +/// ordered, so its canonical bytes key the comparison set.) +fn live_map(rumors: &Rumors) -> BTreeMap, u64> { + rumors + .snapshot() + .iter() + .map(|(v, m)| (v.as_bytes().to_vec(), **m)) + .collect() } /// §6.1 Genesis replay: a from-genesis observer on a populated set yields @@ -86,7 +90,10 @@ fn genesis_replay_observes_the_live_set_once() { "actors are live, so the observer goes quiet, not ended" ); - let observed: BTreeMap = items.iter().map(|(k, _, m)| (*k, *m)).collect(); + let observed: BTreeMap, u64> = items + .iter() + .map(|(v, m)| (v.as_bytes().to_vec(), *m)) + .collect(); assert_eq!(observed.len(), items.len(), "no message is observed twice"); assert_eq!( observed, @@ -94,7 +101,7 @@ fn genesis_replay_observes_the_live_set_once() { "exactly the live set is observed" ); - for (_, version, _) in &items { + for (version, _) in &items { assert!( version <= obs.checkpoint(), "the post-pass checkpoint dominates every observed version" @@ -114,13 +121,13 @@ fn checkpoint_start_observes_only_what_it_does_not_contain() { let mut obs = rumors.unordered_messages_since(v_mid.clone()); let (items, _) = drain(&mut obs); - let observed: BTreeSet = items.iter().map(|(_, _, m)| *m).collect(); + let observed: BTreeSet = items.iter().map(|(_, m)| *m).collect(); assert_eq!( observed, BTreeSet::from([4, 5, 6]), "exactly the leaves above v_mid fire" ); - for (_, version, _) in &items { + for (version, _) in &items { // The causal membership predicate itself: `since(&v_mid)` keeps // exactly the versions v_mid does not contain. assert!( @@ -147,14 +154,14 @@ fn live_sends_and_gossip_learned_messages_are_observed() { sibling.send(10); let (items, _) = drain(&mut obs); assert_eq!(items.len(), 1, "the sibling's send is observed"); - assert_eq!(items[0].2, 10); + assert_eq!(items[0].1, 10); // A message learned through gossip. b.send(20); wire_gossip(&a, &b); let (items, _) = drain(&mut obs); assert_eq!(items.len(), 1, "the gossip-learned message is observed"); - assert_eq!(items[0].2, 20); + assert_eq!(items[0].1, 20); } /// §6.4 Redaction honored: an observed-then-redacted message fires nothing @@ -170,8 +177,8 @@ fn redactions_are_honored_silently() { // Redacted before subscription: never fires. let pre = rumors.snapshot().latest().clone(); rumors.send(1); - let key_1 = minted_key(&rumors.snapshot(), &pre); - rumors.redact(key_1); + let version_1 = minted_version(&rumors.snapshot(), &pre); + rumors.redact(&version_1); let mut obs = rumors.unordered_messages(); let (items, _) = drain(&mut obs); assert!(items.is_empty(), "a pre-subscription redaction never fires"); @@ -179,18 +186,18 @@ fn redactions_are_honored_silently() { // Observed, then redacted: nothing further fires. let pre = rumors.snapshot().latest().clone(); rumors.send(2); - let key_2 = minted_key(&rumors.snapshot(), &pre); + let version_2 = minted_version(&rumors.snapshot(), &pre); let (items, _) = drain(&mut obs); assert_eq!(items.len(), 1, "the live message fires once"); - rumors.redact(key_2); + rumors.redact(&version_2); let (items, _) = drain(&mut obs); assert!(items.is_empty(), "a redaction fires no further observation"); // Inserted and redacted wholly between passes: never delivered. let pre = rumors.snapshot().latest().clone(); rumors.send(3); - let key_3 = minted_key(&rumors.snapshot(), &pre); - rumors.redact(key_3); + let version_3 = minted_version(&rumors.snapshot(), &pre); + rumors.redact(&version_3); let (items, _) = drain(&mut obs); assert!( items.is_empty(), @@ -205,7 +212,7 @@ fn redactions_are_honored_silently() { rumors.send(5); let (items, _) = drain(&mut from_now); assert_eq!(items.len(), 1); - assert_eq!(items[0].2, 5, "only post-subscription content fires"); + assert_eq!(items[0].1, 5, "only post-subscription content fires"); } /// §6.9 Termination: when the last handle on the set drops, the observer @@ -224,7 +231,7 @@ fn observer_drains_the_final_state_then_ends() { assert_eq!( items .into_iter() - .map(|(k, _, m)| (k, m)) + .map(|(v, m)| (v.as_bytes().to_vec(), m)) .collect::>(), expected, "the complete final state is yielded before the end" @@ -264,7 +271,7 @@ fn retire_ends_the_observer() { let (items, ended) = drain(&mut obs); assert!(ended, "retiring the set ends its observers"); assert!( - items.iter().any(|(_, _, m)| *m == 7), + items.iter().any(|(_, m)| *m == 7), "the final drain delivered the retiree's own message" ); } @@ -320,7 +327,7 @@ fn observer_does_not_block_reunite_and_survives_it() { 1, "the observer keeps observing across reunite" ); - assert_eq!(items[0].2, 42); + assert_eq!(items[0].1, 42); } /// §6.12 Non-blocking observer: an observer mid-pass — its most recent item @@ -333,7 +340,7 @@ fn lent_borrows_do_not_block_senders() { let mut obs = rumors.unordered_messages(); let lent = block_on(obs.borrow_next()).expect("first item of the pass"); - let lent_value = *lent.2.clone(); + let lent_value = *lent.1.clone(); // With the borrow conceptually outstanding (the observer is mid-pass), // a send must not deadlock. @@ -341,7 +348,7 @@ fn lent_borrows_do_not_block_senders() { let (rest, _) = drain(&mut obs); assert!( - rest.iter().any(|(_, _, m)| *m == 3), + rest.iter().any(|(_, m)| *m == 3), "the mid-pass send is observed by a later pass" ); assert!( @@ -395,7 +402,7 @@ fn checkpoint_is_portable_across_replicas() { let mut obs_b = b.unordered_messages_since(checkpoint); let (items, _) = drain(&mut obs_b); assert_eq!(items.len(), 1, "only the message A never observed fires"); - assert_eq!(items[0].2, 2, "A-observed messages are skipped at B"); + assert_eq!(items[0].1, 2, "A-observed messages are skipped at B"); } /// The observer's non-blocking step lends exactly as @@ -410,7 +417,7 @@ fn try_next_distinguishes_quiet_from_ended() { let mut obs = rumors.unordered_messages(); let mut seen = BTreeSet::new(); - while let TryNext::Message((_, _, m)) = obs.try_next() { + while let TryNext::Message((_, m)) = obs.try_next() { seen.insert(**m); } assert_eq!(seen, BTreeSet::from([1, 2]), "the pending pass drains"); @@ -420,7 +427,7 @@ fn try_next_distinguishes_quiet_from_ended() { ); rumors.send(3); - let TryNext::Message((_, _, m)) = obs.try_next() else { + let TryNext::Message((_, m)) = obs.try_next() else { panic!("the new send is immediately available"); }; assert_eq!(**m, 3); @@ -445,8 +452,8 @@ fn stream_face_matches_and_terminates() { let mut obs = rumors.unordered_messages(); let mut items = BTreeMap::new(); - while let Some(Some((k, _, m))) = obs.next().now_or_never() { - items.insert(k, *m); + while let Some(Some((v, m))) = obs.next().now_or_never() { + items.insert(v.as_bytes().to_vec(), *m); } assert_eq!(items, expected, "the Stream face yields the live set"); @@ -461,7 +468,8 @@ fn stream_face_matches_and_terminates() { /// §6.6 (negative control): folding *delivered* versions is not a sound /// resume point. /// -/// Delivery is in key order, not causal order, so a stopped +/// Delivery is in path order (the hash of the version), not causal +/// order, so a stopped /// pass can have delivered `m2` (later version) but not `m1` (earlier); /// the fold then causally contains `m1`, and resuming from it skips `m1` /// forever — loss, not re-delivery. `UnorderedMessages::checkpoint()` (the @@ -470,26 +478,29 @@ fn stream_face_matches_and_terminates() { #[test] fn folding_delivered_versions_can_lose_a_message() { // Search deterministic universes for the counterexample shape: the - // *later*-minted of two messages is delivered first (content-addressed - // keys vs. causal versions disagree about order roughly half the time). - let (rumors, later_value) = (1u64..256) - .find_map(|candidate| { - let rumors = Peer::::seed_rng(&mut SmallRng::seed_from_u64(0)) + // *later*-minted of two messages is delivered first. A leaf's path is + // the hash of its version, and paths vs. causal versions disagree + // about order roughly half the time, so varying the universe seed + // (which varies the minted versions) finds the shape quickly. + let later_value = 1u64; + let rumors = (0u64..256) + .find_map(|seed| { + let rumors = Peer::::seed_rng(&mut SmallRng::seed_from_u64(seed)) .sync_window_floor() .into_rumors(); rumors.send(0); - rumors.send(candidate); + rumors.send(later_value); let snapshot = rumors.snapshot(); let first_yielded = snapshot.iter().next().expect("two live messages"); - let later_first = **first_yielded.2 == candidate; + let later_first = **first_yielded.1 == later_value; drop(snapshot); - later_first.then_some((rumors, candidate)) + later_first.then_some(rumors) }) - .expect("some candidate must collide into key-before-version order"); + .expect("some universe must collide into path-before-version order"); // Deliver exactly one item — the later version — and stop mid-pass. let mut obs = rumors.unordered_messages(); - let Step::Item((_, delivered_version, delivered_value)) = step(&mut obs) else { + let Step::Item((delivered_version, delivered_value)) = step(&mut obs) else { panic!("the populated set delivers an item"); }; assert_eq!(delivered_value, later_value, "the later version came first"); @@ -526,7 +537,8 @@ enum Op { /// Send this value (through one of two sibling `Rumors` clones, /// alternating by op index). Send(u64), - /// Redact the `idx % minted`-th key minted so far (dropped if none). + /// Redact the `idx % minted`-th message minted so far (dropped if + /// none). Redact(usize), /// Drain the observer to quiescence. Drain, @@ -546,16 +558,16 @@ fn arb_ops() -> impl Strategy> { proptest! { /// §6.5 Exactly-once under interleaving: across an arbitrary /// send/redact/drain interleaving (sends through alternating sibling - /// clones), no key is ever observed twice, and the observations cover - /// the final live set. + /// clones), no message is ever observed twice, and the observations + /// cover the final live set. #[test] fn exactly_once_under_interleaving(ops in arb_ops()) { let rumors = Peer::::seed().sync_window_floor().into_rumors(); let sibling = rumors.clone(); let mut obs = rumors.unordered_messages(); - let mut minted: Vec = Vec::new(); - let mut observed: Vec<(Key, Version, u64)> = Vec::new(); + let mut minted: Vec = Vec::new(); + let mut observed: Vec<(Version, u64)> = Vec::new(); for (i, op) in ops.iter().enumerate() { match op { @@ -563,11 +575,11 @@ proptest! { let handle = if i % 2 == 0 { &rumors } else { &sibling }; let pre = handle.snapshot().latest().clone(); handle.send(*v); - minted.push(minted_key(&handle.snapshot(), &pre)); + minted.push(minted_version(&handle.snapshot(), &pre)); } Op::Redact(idx) => { if !minted.is_empty() { - rumors.redact(minted[idx % minted.len()]); + rumors.redact(&minted[idx % minted.len()]); } } Op::Drain => { @@ -585,12 +597,17 @@ proptest! { observed.extend(final_items); let mut seen = BTreeSet::new(); - for (key, _, _) in &observed { - prop_assert!(seen.insert(*key), "key {key:?} observed twice"); + for (version, _) in &observed { + prop_assert!( + seen.insert(version.as_bytes().to_vec()), + "version {version:?} observed twice" + ); } for (key, value) in &final_live { prop_assert!( - observed.iter().any(|(k, _, m)| k == key && m == value), + observed + .iter() + .any(|(v, m)| v.as_bytes() == key.as_slice() && m == value), "a final live message was never observed", ); } @@ -623,7 +640,7 @@ proptest! { // Deliver a prefix of the first pass — or, when `complete_pass`, // drain to quiescence so the pass commits into the checkpoint. let mut obs = rumors.unordered_messages(); - let mut first_run: Vec<(Key, Version, u64)> = Vec::new(); + let mut first_run: Vec<(Version, u64)> = Vec::new(); if complete_pass { let (items, _) = drain(&mut obs); first_run.extend(items); @@ -659,24 +676,27 @@ proptest! { first_run .iter() .chain(&second_run) - .any(|(k, _, m)| k == key && m == value), + .any(|(v, m)| v.as_bytes() == key.as_slice() && m == value), "a live message fell between the stopped and resumed observers", ); } - // Re-delivery discipline: a key delivered by both runs must have - // been part of the interrupted pass; after a *completed* pass, - // there are no re-deliveries at all. - let first_keys: BTreeSet = first_run.iter().map(|(k, _, _)| *k).collect(); - let second_keys: BTreeSet = second_run.iter().map(|(k, _, _)| *k).collect(); - let redelivered: Vec<&Key> = first_keys.intersection(&second_keys).collect(); + // Re-delivery discipline: a message delivered by both runs must + // have been part of the interrupted pass; after a *completed* + // pass, there are no re-deliveries at all. + let first_versions: BTreeSet> = + first_run.iter().map(|(v, _)| v.as_bytes().to_vec()).collect(); + let second_versions: BTreeSet> = + second_run.iter().map(|(v, _)| v.as_bytes().to_vec()).collect(); + let redelivered: Vec<&Vec> = + first_versions.intersection(&second_versions).collect(); if complete_pass { prop_assert!( redelivered.is_empty(), "a completed pass's messages must not re-fire: {redelivered:?}", ); } - // (Mid-pass, `redelivered ⊆ first_keys` holds by construction; the - // loss-freedom assertion above is the substantive check.) + // (Mid-pass, `redelivered ⊆ first_versions` holds by construction; + // the loss-freedom assertion above is the substantive check.) } } diff --git a/tests/membership.rs b/tests/membership.rs index 540be642a..0b2456756 100644 --- a/tests/membership.rs +++ b/tests/membership.rs @@ -16,9 +16,9 @@ use std::collections::BTreeMap; use proptest::prelude::*; use proptest::strategy::ValueTree; use proptest::test_runner::TestRunner; -use rumors::{Key, Rumors}; +use rumors::Rumors; -use crate::common::oracle::{readout, readout_multiset}; +use crate::common::oracle::{readout, readout_multiset, version_key}; use crate::common::schedule::events::Event; use crate::common::schedule::{arb_membership_schedule, execute_membership_and_quiesce}; use crate::common::sim::assert_party_invariants; @@ -34,10 +34,10 @@ proptest! { /// 1. every live peer's readout multiset equals the oracle's /// `expected_live()` — content crosses retirements and /// bootstraps without loss or invention; - /// 2. every live peer agrees on the full `Key → value` map (built - /// from the oracle minus its redaction set, so a live redacted - /// key is a mismatch here — deletion honoring survives - /// absorption and newcomer copies); + /// 2. every live peer agrees on the full identity → value map + /// (built from the oracle minus its redaction set, so a live + /// redacted message is a mismatch here — deletion honoring + /// survives absorption and newcomer copies); /// 3. the global party invariants hold sharply: live parties are /// pairwise disjoint and fold-join back to exactly /// `Party::seed()` — the engine runs one session at a time over @@ -50,11 +50,11 @@ proptest! { ) { let result = execute_membership_and_quiesce(&schedule, &windows); let expected = result.oracle.expected_live(); - let canonical: BTreeMap = result - .resolved_keys + let canonical: BTreeMap, u64> = result + .resolved_versions .iter() .filter(|(id, _)| !result.oracle.is_redacted(**id)) - .map(|(id, k)| (*k, result.oracle.all_inserts()[id])) + .map(|(id, v)| (version_key(v), result.oracle.all_inserts()[id])) .collect(); for (i, peer) in result.live() { @@ -65,7 +65,7 @@ proptest! { ); prop_assert_eq!( &actual, &canonical, - "live peer {} readout key→value map does not match canonical", i, + "live peer {} readout identity→value map does not match canonical", i, ); } diff --git a/tests/multi_peer.rs b/tests/multi_peer.rs index 7db8810d6..54c13a591 100644 --- a/tests/multi_peer.rs +++ b/tests/multi_peer.rs @@ -12,9 +12,8 @@ mod common; use std::collections::BTreeMap; use proptest::prelude::*; -use rumors::Key; -use crate::common::oracle::{readout, readout_multiset}; +use crate::common::oracle::{readout, readout_multiset, version_key}; use crate::common::peer::gossip_step; use crate::common::schedule::{Schedule, arb_schedule, execute_and_quiesce}; use crate::common::window::{WindowAssignment, arb_window_assignment}; @@ -34,9 +33,9 @@ proptest! { /// After the final quiesce phase, every peer's live content matches /// every other's. /// - /// Compared via `readout` — the `(Key, value)` lens the oracle checks - /// also use — so the assertion is about live content alone, independent - /// of the per-peer party state. + /// Compared via `readout` — the identity → value lens the oracle + /// checks also use — so the assertion is about live content alone, + /// independent of the per-peer party state. #[test] fn all_peers_converge_after_quiesce( schedule in schedule_u64(), @@ -71,52 +70,53 @@ proptest! { } } - /// Every peer's readout `Key → value` map equals the canonical - /// map built from the originating peers' `Key`s and the oracle's - /// per-insert values, filtered by the oracle's redaction set. + /// Every peer's readout identity → value map equals the canonical + /// map built from the originating peers' minted `Version`s and the + /// oracle's per-insert values, filtered by the oracle's redaction set. /// /// Pins down that every peer converges on exactly the same - /// `Key`s for exactly the same values — no per-peer key drift. + /// `Version`s for exactly the same values — no per-peer identity + /// drift. #[test] - fn keys_stable_across_peers( + fn versions_stable_across_peers( schedule in schedule_u64(), windows in arb_window_assignment(), ) { let result = execute_and_quiesce(&schedule, &windows); - let expected: BTreeMap = result - .resolved_keys + let expected: BTreeMap, u64> = result + .resolved_versions .iter() .filter(|(id, _)| !result.oracle.is_redacted(**id)) - .map(|(id, k)| (*k, result.oracle.all_inserts()[id])) + .map(|(id, v)| (version_key(v), result.oracle.all_inserts()[id])) .collect(); for (i, peer) in result.peers.iter().enumerate() { let actual = readout(&peer.local.snapshot()); prop_assert_eq!( &actual, &expected, - "peer {} readout key→value map does not match canonical", i, + "peer {} readout identity→value map does not match canonical", i, ); } } - /// No `Key` is observed more than once at any peer across the + /// No message is observed more than once at any peer across the /// entire schedule: re-gossip with an already-known message must /// not re-surface it in the observation log. #[test] - fn each_key_observed_at_most_once_per_peer( + fn each_message_observed_at_most_once_per_peer( schedule in schedule_u64(), windows in arb_window_assignment(), ) { let result = execute_and_quiesce(&schedule, &windows); for (i, peer) in result.peers.iter().enumerate() { - let mut counts: BTreeMap = BTreeMap::new(); - for (k, _, _) in peer.observations.iter() { - *counts.entry(*k).or_insert(0) += 1; + let mut counts: BTreeMap, usize> = BTreeMap::new(); + for (v, _) in peer.observations.iter() { + *counts.entry(version_key(v)).or_insert(0) += 1; } for (k, c) in &counts { prop_assert_eq!( *c, 1, - "peer {} observed key {:?} {} times (must be at most once)", + "peer {} observed version {:?} {} times (must be at most once)", i, k, c, ); } @@ -180,7 +180,7 @@ proptest! { } /// String-T variant of `readout_matches_oracle_after_quiesce`, - /// exercising the borsh round-trip for a non-primitive value + /// exercising the wire round-trip for a non-primitive value /// type. Catches any serialization-path bug invisible to /// fixed-size scalars. #[test] diff --git a/tests/opening_supply.rs b/tests/opening_supply.rs index ec5a1beab..90b81d0c5 100644 --- a/tests/opening_supply.rs +++ b/tests/opening_supply.rs @@ -14,9 +14,10 @@ mod common; use rand::SeedableRng; use rand::rngs::SmallRng; -use rumors::{Peer, Rumors}; +use rumors::{Peer, Rumors, Version}; use crate::common::gossip_snapshot::capture_gossip; +use crate::common::shape::{ballast_avoiding, keep_only, path_radix, pool, send_pool}; use crate::common::wire::{block_on, bootstrap_fork_async}; /// A peer seeded from a fixed RNG so the capture is deterministic. @@ -24,18 +25,17 @@ fn seeded() -> Rumors { Peer::seed_rng(&mut SmallRng::seed_from_u64(0)).into_rumors() } -/// A second message whose key shares its first byte with message `1`'s in -/// this staging (keys `09 a7` and `09 5a`, found by search), so the two -/// sides dispute one root child. +/// Pool size for the one-byte path search staging the disputed sibling. /// -/// The initiator holds both leaves, the -/// responder — forked between the two sends — only the first. -const DISPUTED_SIBLING_VALUE: u64 = 165; +/// The search must hit one *specific* root radix, a direct-hit search with +/// mean 256, so the pool is sized well past it (`common::shape` explains +/// the search-and-redact staging; it is deterministic under the seeded +/// universe). +const RADIX_POOL: u64 = 2048; -/// First of three consecutive responder ballast values whose keys' first -/// bytes (`08`, `b6`, `ef`) avoid the disputed radix (`09`) and make the -/// responder the larger set, so the disputed-subtree holder initiates. -const BALLAST_FROM: u64 = 100; +/// Payload base and pool size for the responder ballast: a disjoint +/// payload range so pool cleanups never touch the other side's messages. +const BALLAST_POOL: (u64, u64) = (10_000, 16); /// Count the frames whose semantic label starts with `label` in a rendered /// wire capture, across both directions. @@ -62,30 +62,50 @@ fn frames_labeled(capture: &str, label: &str) -> usize { /// double exactly this count. #[test] fn divergent_root_child_has_one_question_owner() { + // Stage: message `1` exists before the fork, so both sides hold it; a + // pool search lands a second initiator leaf under the same root radix + // (the disputed child), and the responder keeps three ballast leaves + // outside that radix, making it the larger set. let (a, b) = block_on(async { let a: Rumors = seeded(); a.send(1); let b = bootstrap_fork_async(&a).await; - a.send(DISPUTED_SIBLING_VALUE); - let y = BALLAST_FROM; - b.batch().send(y).send(y + 1).send(y + 2); (a, b) }); + let radix = path_radix( + &a.snapshot() + .iter() + .find_map(|(v, m)| (**m == 1).then_some(v.clone())) + .expect("message 1 is live"), + ); + send_pool(&a, 2, RADIX_POOL); + let sibling = pool(&a, 2, RADIX_POOL) + .into_iter() + .find(|(_, v)| path_radix(v) == radix) + .map(|(value, _)| value) + .expect("some pool leaf lands under message 1's radix"); + keep_only(&a, 2, RADIX_POOL, &[1, sibling]); + let (ballast_from, ballast_pool) = BALLAST_POOL; + send_pool(&b, ballast_from, ballast_pool); + let ballast = ballast_avoiding(&pool(&b, ballast_from, ballast_pool), radix, 3); + keep_only(&b, ballast_from, ballast_pool, &ballast); // Fixture self-checks: one shared radix, disputed; the subtree holder - // is the smaller set and initiates. - let akeys: Vec = a - .snapshot() - .iter() - .map(|(k, _, _)| k.as_bytes()[0]) - .collect(); - assert_eq!(akeys.len(), 2, "the initiator holds the sibling pair"); - assert_eq!(akeys.first(), akeys.last(), "the pair shares a root radix"); - let radix = akeys[0]; + // is the smaller set and initiates. A leaf's path is the full-width + // BLAKE3 hash of its version's canonical bytes. + let path_radix = |version: &Version| blake3::hash(version.as_bytes()).as_bytes()[0]; + let apaths: Vec = a.snapshot().iter().map(|(v, _)| path_radix(v)).collect(); + assert_eq!(apaths.len(), 2, "the initiator holds the sibling pair"); + assert_eq!( + apaths.first(), + apaths.last(), + "the pair shares a root radix" + ); + let radix = apaths[0]; assert_eq!( b.snapshot() .iter() - .filter(|(k, _, _)| k.as_bytes()[0] == radix) + .filter(|(v, _)| path_radix(v) == radix) .count(), 1, "the responder holds exactly one leaf under the disputed radix" diff --git a/tests/pairwise.rs b/tests/pairwise.rs index c6fb83556..846822c9b 100644 --- a/tests/pairwise.rs +++ b/tests/pairwise.rs @@ -6,9 +6,9 @@ //! order-independence across three peers, and the union of live content — //! plus the causal-concurrency basics the merge rests on. //! -//! Live content is compared through `readout` (the `(Key, value)` lens the -//! oracle checks also use) or through `hash`/`latest` where the assertion -//! is "nothing changed at all". +//! Live content is compared through `readout` (the identity → value lens +//! the oracle checks also use) or through `hash`/`latest` where the +//! assertion is "nothing changed at all". //! //! Every peer in a test is a genuine, party-disjoint fork of one shared //! [`Peer::seed`](rumors::Peer::seed), minted by [`bootstrap_fork`]. They @@ -18,7 +18,6 @@ mod common; -use borsh::{BorshDeserialize, BorshSerialize}; use proptest::prelude::*; use rumors::{Rumors, Version, causally}; @@ -26,11 +25,13 @@ use crate::common::action::{arb_local_actions, build_local}; use crate::common::oracle::readout; use crate::common::wire::{bootstrap_fork, wire_gossip}; +use serde::Serialize; +use serde::de::DeserializeOwned; /// A genuine, party-disjoint copy of `k`'s content: a fresh originator that /// holds the same live messages but ticks its own party region. fn dup(k: &Rumors) -> Rumors where - T: Clone + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Serialize + DeserializeOwned + Send + Sync + 'static, { bootstrap_fork(k) } @@ -192,7 +193,7 @@ proptest! { let pre_a = alice.snapshot().latest().clone(); alice.send(a_value); let snap_a = alice.snapshot(); - let (_, va, _) = snap_a + let (va, _) = snap_a .range(causally::since(&pre_a)) .next() .expect("alice's insert mints a live leaf"); @@ -200,7 +201,7 @@ proptest! { let pre_b = bob.snapshot().latest().clone(); bob.send(b_value); let snap_b = bob.snapshot(); - let (_, vb, _) = snap_b + let (vb, _) = snap_b .range(causally::since(&pre_b)) .next() .expect("bob's insert mints a live leaf"); @@ -212,9 +213,9 @@ proptest! { /// equals the union of the two pre-session readouts. /// /// The "union of readouts" is computed by `BTreeMap::extend`, - /// which is sound here only because `Key`s derive from the leaf - /// version's canonical bytes and `alice` / `bob` tick disjoint - /// parties, so they can't mint the same `Key`. + /// which is sound here only because readout keys are the leaf + /// versions' canonical bytes and `alice` / `bob` tick disjoint + /// parties, so they can't mint the same version. #[test] fn gossip_unions_content( a_actions in arb_local_actions(), diff --git a/tests/partition.rs b/tests/partition.rs index e8b7757f8..0f797b2f7 100644 --- a/tests/partition.rs +++ b/tests/partition.rs @@ -8,8 +8,9 @@ //! //! We deliberately do *not* compare against an unrestricted run of //! the same schedule. Doing so would assume order-independence of -//! redactions, but a `redact(K)` event can only happen at peer `P` -//! if `P` has already received `K` via an `on_message` callback — +//! redactions, but a redact event can only happen at peer `P` once +//! `P` has already received the targeted message via an `on_message` +//! callback — //! which is a function of the gossip schedule. A partitioned schedule //! may legitimately suppress some redacts (because the targeted peer //! hasn't observed the key yet), so the two schedules can converge @@ -38,8 +39,8 @@ proptest! { /// allowed only within each side of the split at `split_at`; /// after that, any gossip event is allowed and a final quiesce /// drives the network to convergence. `execute_with` already - /// honors the "only redact a `Key` you've observed" invariant, - /// so redacts whose target keys never crossed the partition are + /// honors the "only redact a message you've observed" invariant, + /// so redacts whose targets never crossed the partition are /// silently skipped — matching what real application code could /// have issued. #[test] diff --git a/tests/redaction.rs b/tests/redaction.rs index 07842befb..4dbe9bb6c 100644 --- a/tests/redaction.rs +++ b/tests/redaction.rs @@ -12,7 +12,6 @@ use std::collections::BTreeMap; use proptest::collection::vec; use proptest::prelude::*; -use rumors::Key; use crate::common::oracle::readout_multiset; use crate::common::peer::{Peer, gossip_step, quiesce}; @@ -38,7 +37,7 @@ proptest! { .map(|_| Peer::new(bootstrap_fork(&seed))) .collect(); - let key = peers[0].insert_one(value); + let version = peers[0].insert_one(value); quiesce(&mut peers); for peer in &peers { @@ -47,7 +46,7 @@ proptest! { } let r = redactor_idx % n_peers; - peers[r].redact_one(key); + peers[r].redact_one(&version); quiesce(&mut peers); for (i, peer) in peers.iter().enumerate() { @@ -60,7 +59,7 @@ proptest! { } /// Two peers each insert several values, then each redacts one of - /// its own keys. The converged content is the same regardless of + /// its own messages. The converged content is the same regardless of /// which side issues its redaction first across the gossip /// boundary. #[test] @@ -72,18 +71,18 @@ proptest! { let seed = rumors::Peer::::seed().sync_window_floor().into_rumors(); let mut a = Peer::new(bootstrap_fork(&seed)); let mut b = Peer::new(bootstrap_fork(&seed)); - let mut a_keys: Vec = Vec::new(); - let mut b_keys: Vec = Vec::new(); - for v in &a_values { a_keys.push(a.insert_one(*v)); } - for v in &b_values { b_keys.push(b.insert_one(*v)); } + let mut a_versions: Vec = Vec::new(); + let mut b_versions: Vec = Vec::new(); + for v in &a_values { a_versions.push(a.insert_one(*v)); } + for v in &b_values { b_versions.push(b.insert_one(*v)); } if a_first { - a.redact_one(a_keys[0]); + a.redact_one(&a_versions[0]); gossip_step(&mut a, &mut b); - b.redact_one(b_keys[0]); + b.redact_one(&b_versions[0]); } else { - b.redact_one(b_keys[0]); + b.redact_one(&b_versions[0]); gossip_step(&mut a, &mut b); - a.redact_one(a_keys[0]); + a.redact_one(&a_versions[0]); } let mut peers = [a, b]; quiesce(&mut peers); @@ -92,25 +91,25 @@ proptest! { prop_assert_eq!(run(true), run(false)); } - /// Redacting the same `Key` a second time is idempotent: the live + /// Redacting the same message a second time is idempotent: the live /// readout is unchanged and nothing new is observed. (The second /// redact is a nil action — the leaf is already gone.) #[test] fn redact_twice_is_idempotent(value in any::()) { let mut peer = Peer::::new(rumors::Peer::seed().sync_window_floor().into_rumors()); - let key = peer.insert_one(value); - peer.redact_one(key); + let version = peer.insert_one(value); + peer.redact_one(&version); let readout_before = readout_multiset(&peer.local.snapshot()); let obs_before = peer.observations.len(); - peer.redact_one(key); + peer.redact_one(&version); prop_assert_eq!(readout_multiset(&peer.local.snapshot()), readout_before); prop_assert_eq!(peer.observations.len(), obs_before); } - /// Redacting a `Key` minted on a different peer that this peer + /// Redacting a message minted on a different peer that this peer /// has never observed has no effect on live content and is not /// observed. /// @@ -118,16 +117,16 @@ proptest! { /// future regressions surface; the public docs are silent on /// this corner. #[test] - fn redact_unknown_key_is_noop(value in any::()) { + fn redact_unknown_version_is_noop(value in any::()) { let seed = rumors::Peer::::seed().sync_window_floor().into_rumors(); let mut bob = Peer::new(bootstrap_fork(&seed)); - let foreign_key = bob.insert_one(value); + let foreign_version = bob.insert_one(value); let mut alice = Peer::new(bootstrap_fork(&seed)); let readout_before = readout_multiset(&alice.local.snapshot()); let obs_before = alice.observations.len(); - alice.redact_one(foreign_key); + alice.redact_one(&foreign_version); prop_assert_eq!(readout_multiset(&alice.local.snapshot()), readout_before); prop_assert_eq!(alice.observations.len(), obs_before); diff --git a/tests/retire.rs b/tests/retire.rs index 7d3dfe8eb..85a17698c 100644 --- a/tests/retire.rs +++ b/tests/retire.rs @@ -172,7 +172,7 @@ fn divergent_retiree_reconciles_then_retires() { matches!(outcome, Retire::Retired), "the in-session gossip round brings the peer to dominance, got {outcome:?}" ); - let mut live: Vec = b.snapshot().iter().map(|(_, _, m)| **m).collect(); + let mut live: Vec = b.snapshot().iter().map(|(_, m)| **m).collect(); live.sort_unstable(); assert_eq!( live, @@ -186,27 +186,28 @@ fn divergent_retiree_reconciles_then_retires() { /// exactly as a plain gossip session would have spread it. #[test] fn retiree_redaction_propagates_through_retire() { - // Both peers hold 1 and 2 (inserted before the fork, so the keys are - // shared); the retiree then redacts 1 while the peer inserts 3. + // Both peers hold 1 and 2 (inserted before the fork, so the messages + // and their versions are shared); the retiree then redacts 1 while the + // peer inserts 3. let seed = Peer::::seed().sync_window_floor().into_rumors(); seed.batch().send(1).send(2); - let key_of_1 = seed + let version_of_1 = seed .snapshot() .iter() - .find_map(|(k, _, m)| (**m == 1).then_some(k)) - .expect("key recorded for 1"); + .find_map(|(v, m)| (**m == 1).then_some(v.clone())) + .expect("version recorded for 1"); let a = bootstrap_fork(&seed); let b = async_known(seed, &[3]); - a.redact(key_of_1); + a.redact(&version_of_1); let outcome = retire_into_gossip(a, &b); assert!( matches!(outcome, Retire::Retired), "the reconciled peer absorbs the retiree, got {outcome:?}" ); - let mut live: Vec = b.snapshot().iter().map(|(_, _, m)| **m).collect(); + let mut live: Vec = b.snapshot().iter().map(|(_, m)| **m).collect(); live.sort_unstable(); assert_eq!( live, @@ -287,7 +288,7 @@ fn retire_into_bootstrapper_hands_off_the_identity() { successor.send(99); wire_gossip(&successor, &seed); assert!( - seed.snapshot().iter().any(|(_, _, m)| **m == 99), + seed.snapshot().iter().any(|(_, m)| **m == 99), "the successor's origination survives gossip" ); } diff --git a/tests/retire_redaction.rs b/tests/retire_redaction.rs index 4bfde9ebe..f46d2930d 100644 --- a/tests/retire_redaction.rs +++ b/tests/retire_redaction.rs @@ -22,20 +22,20 @@ fn retire_carries_last_minute_redactions() { // B originates an entry and A learns it through ordinary gossip. b.send("presence: b".to_string()); - let key = b + let version = b .snapshot() .iter() - .map(|(key, _, _)| key) + .map(|(version, _)| version.clone()) .next() .expect("the sent entry is live"); wire_gossip(&a, &b); assert!( - a.snapshot().get(&key).is_some(), + a.snapshot().get(&version).is_some(), "precondition: A holds B's entry after gossip" ); // B redacts it *after* that gossip, then retires into A. - b.redact(key); + b.redact(&version); let retiree = block_on(b.try_into_peer()).expect("sole handle"); let outcome = block_on(async { let (mut b_link, mut a_link) = rumors::link::memory(); @@ -48,7 +48,7 @@ fn retire_carries_last_minute_redactions() { // The absorber holds the absence, not the ghost. assert!( - a.snapshot().get(&key).is_none(), + a.snapshot().get(&version).is_none(), "A must honor the redaction the retiree carried" ); } diff --git a/tests/session_overlap.rs b/tests/session_overlap.rs index 553fdc867..b6679fb71 100644 --- a/tests/session_overlap.rs +++ b/tests/session_overlap.rs @@ -21,7 +21,7 @@ use common::oracle::{readout, readout_multiset}; use common::overlap::{self, arb_overlap_schedule, execute_overlap_and_quiesce}; use common::wire::{bootstrap_fork, wire_gossip}; use proptest::prelude::*; -use rumors::{Key, Rumors}; +use rumors::{Rumors, Version}; /// Build the deterministic witness fleet: a seed peer holding `n` unit /// messages and two bootstrapped forks, all converged (a bootstrap copies @@ -40,7 +40,7 @@ fn converged_trio(n: u64) -> (Rumors, Rumors, Rumors) { /// /// Panics if they fail to agree within a bounded number of full-mesh /// rounds: overlapped sessions must still converge. -fn converge(a: &Rumors, b: &Rumors, c: &Rumors) -> BTreeMap { +fn converge(a: &Rumors, b: &Rumors, c: &Rumors) -> BTreeMap, u64> { const ROUNDS: usize = 8; for _ in 0..ROUNDS { wire_gossip(a, b); @@ -92,17 +92,20 @@ fn overlapped_install_never_loses_innocent_messages() { polls }; - let keys: Vec = { + // The versions minted by `converged_trio` are deterministic (the + // seed party and its tick sequence are fixed), so versions read from + // one instance name the same messages in every other. + let versions: Vec = { let (a, _, _) = converged_trio(MESSAGES); - readout(&a.snapshot()).into_keys().collect() + a.snapshot().iter().map(|(v, _)| v.clone()).collect() }; let mut violations = Vec::new(); - for &redacted in &keys { + for redacted in &versions { for n in 0..=session_polls { let (a, b, c) = converged_trio(MESSAGES); let mut expected = readout(&a.snapshot()); - expected.remove(&redacted); + expected.remove(redacted.as_bytes()); b.redact(redacted); // S2 (A <-> C, both still converged at fork time) opens @@ -119,14 +122,14 @@ fn overlapped_install_never_loses_innocent_messages() { let converged = converge(&a, &b, &c); if converged != expected { - violations.push((redacted, n, converged.len(), expected.len())); + violations.push((redacted.clone(), n, converged.len(), expected.len())); } } } assert!( violations.is_empty(), "overlapped installs diverged from the one deliberate redaction \ - (redacted key, S2 poll prefix, converged len, expected len): {violations:?}", + (redacted version, S2 poll prefix, converged len, expected len): {violations:?}", ); } @@ -138,7 +141,7 @@ proptest! { /// Generated overlapping-session schedules converge to the oracle. /// - /// Fleets of 2–4 peers run schedules mixing sends, observed-key + /// Fleets of 2–4 peers run schedules mixing sends, observed-message /// redactions, whole sessions, and sessions opened, parked at /// generated poll prefixes, and closed across other events — /// starting from a converged base large enough to span several @@ -166,8 +169,9 @@ proptest! { i, ); } - // Key-level identity across peers, not just value multisets: - // the same content must live at the same keys everywhere. + // Identity-level agreement across peers, not just value + // multisets: the same content must live at the same versions + // everywhere. for pair in readouts.windows(2) { prop_assert_eq!(readout(&pair[0]), readout(&pair[1])); } diff --git a/tests/session_stats.rs b/tests/session_stats.rs index ae3dbed1e..d7b7530cd 100644 --- a/tests/session_stats.rs +++ b/tests/session_stats.rs @@ -25,11 +25,13 @@ use tokio::io::AsyncWrite; use crate::common::wire::{LINK_BUF, assert_control_drained, block_on, bootstrap_fork_async}; +use serde::Serialize; +use serde::de::DeserializeOwned; /// Run one gossip session between two handles over an in-memory link, /// returning both sides' [`Gossiped`]. async fn gossip_pair(a: &Rumors, b: &Rumors) -> (Gossiped, Gossiped) where - T: borsh::BorshSerialize + borsh::BorshDeserialize + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { let (mut a_link, mut b_link) = rumors::link::memory_with_capacity(LINK_BUF); let (a_out, b_out) = tokio::join!(a.gossip(&mut a_link), b.gossip(&mut b_link)); @@ -95,13 +97,13 @@ fn honored_redaction_counts_as_shed() { let a: Rumors = Peer::seed().sync_window_floor().into_rumors(); a.batch().send(10).send(20); let b = bootstrap_fork_async(&a).await; - let key = a + let version = a .snapshot() .iter() - .find(|(_, _, value)| ***value == 10) - .map(|(key, _, _)| key) + .find(|(_, value)| ***value == 10) + .map(|(version, _)| version.clone()) .expect("the sent message is live"); - a.redact(key); + a.redact(&version); let (a_g, b_g) = gossip_pair(&a, &b).await; assert_eq!(b_g.stats.messages_shed, 1, "b honors a's deletion"); diff --git a/tests/shadow_validity.rs b/tests/shadow_validity.rs index 33d4a8b85..33dcf4f4d 100644 --- a/tests/shadow_validity.rs +++ b/tests/shadow_validity.rs @@ -18,7 +18,7 @@ //! peer, its complete lifetime log). //! * `live` — the set of `EventIdx`s the shadow predicts each peer //! still holds at the end of the schedule must match the live -//! peer's readout (translated through `resolved_keys`). +//! peer's readout (translated through `resolved_versions`). //! * `alive` — under membership events, exactly the slots the shadow //! predicts alive must have survived. //! @@ -30,9 +30,8 @@ mod common; use std::collections::{BTreeMap, BTreeSet}; use proptest::prelude::*; -use rumors::Key; -use crate::common::oracle::readout; +use crate::common::oracle::{readout, version_key}; use crate::common::schedule::{ EventIdx, arb_membership_schedule_with_shadow, arb_schedule_with_shadow, execute_membership, execute_with, @@ -46,21 +45,24 @@ proptest! { /// For every peer, the shadow simulator's `observed_log` and /// `live` sets (as `BTreeSet`) match the live /// executor's observations and current readout, translated - /// through `resolved_keys` back to event indices. + /// through `resolved_versions` back to event indices. #[test] fn shadow_predicts_live_state( (schedule, shadow) in arb_schedule_with_shadow(any::(), N_PEERS, MAX_EVENTS), windows in arb_window_assignment(), ) { let result = execute_with(&schedule, &windows, |_, _, _| true); - let key_to_event_idx: BTreeMap = - result.resolved_keys.iter().map(|(eid, k)| (*k, *eid)).collect(); + let version_to_event_idx: BTreeMap, EventIdx> = result + .resolved_versions + .iter() + .map(|(eid, v)| (version_key(v), *eid)) + .collect(); for (p, peer) in result.peers.iter().enumerate() { let live_observed: BTreeSet = peer .observations .iter() - .map(|(k, _, _)| key_to_event_idx[k]) + .map(|(v, _)| version_to_event_idx[v.as_bytes()]) .collect(); let predicted_observed: BTreeSet = shadow.observed_log[p].iter().copied().collect(); @@ -71,7 +73,7 @@ proptest! { let live_held: BTreeSet = readout(&peer.local.snapshot()) .into_keys() - .map(|k| key_to_event_idx[&k]) + .map(|k| version_to_event_idx[&k]) .collect(); prop_assert_eq!( live_held, shadow.live[p].clone(), @@ -96,8 +98,11 @@ proptest! { windows in arb_window_assignment(), ) { let result = execute_membership(&schedule, &windows); - let key_to_event_idx: BTreeMap = - result.resolved_keys.iter().map(|(eid, k)| (*k, *eid)).collect(); + let version_to_event_idx: BTreeMap, EventIdx> = result + .resolved_versions + .iter() + .map(|(eid, v)| (version_key(v), *eid)) + .collect(); prop_assert_eq!( result.slots.len(), shadow.alive.len(), @@ -108,16 +113,20 @@ proptest! { slot.is_some(), shadow.alive[p], "peer {} aliveness disagrees with shadow", p, ); - let observations: Vec = match slot { - Some(peer) => peer.observations.iter().map(|(k, _, _)| *k).collect(), + let observations: Vec> = match slot { + Some(peer) => peer + .observations + .iter() + .map(|(v, _)| version_key(v)) + .collect(), None => result.retired_observations[&p] .iter() - .map(|(k, _, _)| *k) + .map(|(v, _)| version_key(v)) .collect(), }; let live_observed: BTreeSet = observations .iter() - .map(|k| key_to_event_idx[k]) + .map(|k| version_to_event_idx[k]) .collect(); let predicted_observed: BTreeSet = shadow.observed_log[p].iter().copied().collect(); @@ -129,7 +138,7 @@ proptest! { if let Some(peer) = slot { let live_held: BTreeSet = readout(&peer.local.snapshot()) .into_keys() - .map(|k| key_to_event_idx[&k]) + .map(|k| version_to_event_idx[&k]) .collect(); prop_assert_eq!( live_held, shadow.live[p].clone(), diff --git a/tests/single_peer.rs b/tests/single_peer.rs index ccca3b4af..cee4af3a4 100644 --- a/tests/single_peer.rs +++ b/tests/single_peer.rs @@ -1,9 +1,9 @@ //! Single-peer correctness for a lone rumor set, with no gossip. //! //! Exercises the surface area of [`Batch`](rumors::Batch) commits: -//! live-leaf fan-out, `Key` distinctness within a batch, and strict -//! monotonicity of the local party's component of each minted -//! [`Version`](rumors::Version). +//! live-leaf fan-out, distinctness of the [`Version`](rumors::Version)s +//! minted within a batch, and strict monotonicity of the local party's +//! component of each minted version. mod common; @@ -11,14 +11,15 @@ use std::collections::{BTreeMap, BTreeSet}; use proptest::collection::vec; use proptest::prelude::*; -use rumors::{Key, Peer, Rumors, Version, causally}; +use rumors::{Peer, Rumors, Version, causally}; use crate::common::wire::block_on; -/// Commit `values` to `peer` as one batch, returning the `(Key, Version)` -/// pairs it minted (recovered as the live leaves above the pre-commit -/// frontier). -fn batch_send(peer: &Rumors, values: &[u64]) -> Vec<(Key, Version)> { +use serde::Serialize; +use serde::Serializer; +/// Commit `values` to `peer` as one batch, returning the [`Version`]s it +/// minted (recovered as the live leaves above the pre-commit frontier). +fn batch_send(peer: &Rumors, values: &[u64]) -> Vec { let pre = peer.snapshot().latest().clone(); { let mut batch = peer.batch(); @@ -28,7 +29,7 @@ fn batch_send(peer: &Rumors, values: &[u64]) -> Vec<(Key, Version)> { } peer.snapshot() .range(causally::since(&pre)) - .map(|(k, v, _)| (k, v.clone())) + .map(|(v, _)| v.clone()) .collect() } @@ -43,26 +44,29 @@ proptest! { prop_assert_eq!(peer.snapshot().len(), values.len()); } - /// All `Key`s minted within a single batch are distinct, even when - /// several values in the batch are equal. + /// All `Version`s minted within a single batch are distinct, even + /// when several values in the batch are equal. #[test] - fn distinct_keys_per_batch(values in vec(any::(), 1..=32)) { + fn distinct_versions_per_batch(values in vec(any::(), 1..=32)) { let peer = Peer::::seed().sync_window_floor().into_rumors(); let minted = batch_send(&peer, &values); prop_assert_eq!(minted.len(), values.len()); - let unique: BTreeSet<_> = minted.iter().map(|(k, _)| *k).collect(); - prop_assert_eq!(unique.len(), values.len(), "keys must be distinct"); + let unique: BTreeSet<_> = + minted.iter().map(|v| v.as_bytes().to_vec()).collect(); + prop_assert_eq!(unique.len(), values.len(), "versions must be distinct"); } - /// The same value inserted `n` times in one batch still yields - /// `n` distinct `Key`s — content equality does not collapse keys. + /// The same value inserted `n` times in one batch still yields `n` + /// distinct leaves — each send mints a fresh `Version`, so content + /// equality does not collapse messages. #[test] - fn duplicate_values_get_distinct_keys(n in 1usize..=16, value in any::()) { + fn duplicate_values_get_distinct_versions(n in 1usize..=16, value in any::()) { let peer = Peer::::seed().sync_window_floor().into_rumors(); let values: Vec = std::iter::repeat_n(value, n).collect(); let minted = batch_send(&peer, &values); prop_assert_eq!(minted.len(), n); - let unique: BTreeSet<_> = minted.iter().map(|(k, _)| *k).collect(); + let unique: BTreeSet<_> = + minted.iter().map(|v| v.as_bytes().to_vec()).collect(); prop_assert_eq!(unique.len(), n); } @@ -84,8 +88,7 @@ proptest! { // order. Each batch's recovery is scoped by the pre-commit frontier. let mut versions: Vec = Vec::new(); for batch in &batches { - let mut minted: Vec = - batch_send(&peer, batch).into_iter().map(|(_, v)| v).collect(); + let mut minted: Vec = batch_send(&peer, batch); minted.sort_by(|a, b| { a.partial_cmp(b).expect("a lone peer's versions are totally ordered") }); @@ -134,7 +137,7 @@ proptest! { let peer = Peer::::seed().sync_window_floor().into_rumors(); batch_send(&peer, values); let mut out = BTreeMap::new(); - for (_, _, v) in peer.snapshot().iter() { + for (_, v) in peer.snapshot().iter() { *out.entry(**v).or_insert(0) += 1; } out @@ -152,12 +155,12 @@ struct Explosive { fail: bool, } -impl borsh::BorshSerialize for Explosive { - fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { +impl Serialize for Explosive { + fn serialize(&self, serializer: S) -> Result { if self.fail { - return Err(borsh::io::Error::other("detonated")); + return Err(serde::ser::Error::custom("detonated")); } - borsh::BorshSerialize::serialize(&self.value, writer) + self.value.serialize(serializer) } } diff --git a/tests/snapshots/bootstrap_snapshot__empty_provider.snap b/tests/snapshots/bootstrap_snapshot__empty_provider.snap index 9a0b0fa5d..095083aa9 100644 --- a/tests/snapshots/bootstrap_snapshot__empty_provider.snap +++ b/tests/snapshots/bootstrap_snapshot__empty_provider.snap @@ -16,8 +16,8 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 6 bytes 0000: 00 00 00 01 48 2e @@ -35,7 +35,7 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 1 bytes 0000: 2e diff --git a/tests/snapshots/bootstrap_snapshot__mutual_bootstrap_bails.snap b/tests/snapshots/bootstrap_snapshot__mutual_bootstrap_bails.snap index 5fb4e3086..72e15a880 100644 --- a/tests/snapshots/bootstrap_snapshot__mutual_bootstrap_bails.snap +++ b/tests/snapshots/bootstrap_snapshot__mutual_bootstrap_bails.snap @@ -16,8 +16,8 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 1 bytes 0000: 2e @@ -35,7 +35,7 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 1 bytes 0000: 2e diff --git a/tests/snapshots/bootstrap_snapshot__populated_provider.snap b/tests/snapshots/bootstrap_snapshot__populated_provider.snap index d5e93fc7d..1542400b4 100644 --- a/tests/snapshots/bootstrap_snapshot__populated_provider.snap +++ b/tests/snapshots/bootstrap_snapshot__populated_provider.snap @@ -16,40 +16,36 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 92 listing: 3 child(ren) - child 0x9: 7ca8b07cb28254109eb3f89675cdb22ab6882b6ab31b3cdf - child 0xa0: b7e99b381ba2cb4d2f4fef624ed06674d9e0a367d05272d6 - child 0xc4: bfa8f59b4a414e5335b1c4a323b90c6fa7529bcf7ca58216 -listing frame: 83 bytes - 0000: 00 00 00 4f 03 00 00 00 - 0008: 09 7c a8 b0 7c b2 82 54 - 0010: 10 9e b3 f8 96 75 cd b2 - 0018: 2a b6 88 2b 6a b3 1b 3c - 0020: df a0 b7 e9 9b 38 1b a2 - 0028: cb 4d 2f 4f ef 62 4e d0 - 0030: 66 74 d9 e0 a3 67 d0 52 - 0038: 72 d6 c4 bf a8 f5 9b 4a - 0040: 41 4e 53 35 b1 c4 a3 23 - 0048: b9 0c 6f a7 52 9b cf 7c - 0050: a5 82 16 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa + child 0x88: cf55a19b320fee28bbdd5998cabc6c0000caeb8c569ad975 + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 +listing frame: 79 bytes + 0000: 00 00 00 4b 7c f5 3f 94 + 0008: 77 30 97 88 c6 ed 88 34 + 0010: 00 8f 39 4a 3b e5 f2 12 + 0018: 95 9f ac 55 fa 88 cf 55 + 0020: a1 9b 32 0f ee 28 bb dd + 0028: 59 98 ca bc 6c 00 00 ca + 0030: eb 8c 56 9a d9 75 9a cc + 0038: a7 fd 1f e9 57 c5 26 7c + 0040: e1 59 c8 f3 fe 95 bc 4f + 0048: 49 6f 6b 2f b5 fa 39 Responder stream 0 (height 31), epoch 0 frame 0: Supply(Continue) supply run: 1 record(s) - record 0: version 1, message 8 byte(s) - 0000: 66 00 00 00 0d 00 00 00 - 0008: 09 a8 01 00 00 00 00 00 - 0010: 00 00 + record 0: version 2, message 1 byte(s) + 0000: 66 00 00 00 07 00 00 00 + 0008: 03 41 b8 02 frame 1: Supply(Continue) supply run: 1 record(s) - record 0: version 2, message 8 byte(s) - 0000: 66 00 00 00 0d 00 00 00 - 0008: 09 b8 02 00 00 00 00 00 - 0010: 00 00 + record 0: version 3, message 1 byte(s) + 0000: 66 00 00 00 07 00 00 00 + 0008: 03 41 92 03 frame 2: Supply(End) supply run: 1 record(s) - record 0: version 3, message 8 byte(s) - 0000: 77 00 00 00 0d 00 00 00 - 0008: 09 92 03 00 00 00 00 00 - 0010: 00 00 + record 0: version 1, message 1 byte(s) + 0000: 77 00 00 00 07 00 00 00 + 0008: 03 41 a8 01 frame 3: End(Stream) 0000: 99 trailing frame: 6 bytes @@ -69,7 +65,7 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 1 bytes 0000: 2e diff --git a/tests/snapshots/bootstrap_snapshot__string_payload.snap b/tests/snapshots/bootstrap_snapshot__string_payload.snap index cde7ed78f..8656572e4 100644 --- a/tests/snapshots/bootstrap_snapshot__string_payload.snap +++ b/tests/snapshots/bootstrap_snapshot__string_payload.snap @@ -16,30 +16,29 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 b8 listing: 2 child(ren) - child 0x51: 0dcf9a1cf89f020bbeaafcf37bfdbe1eceab3a1c0b73f902 - child 0xbe: 6afa3ed7e992a60fd6dca61760b8505eb3060be1d8ab6014 -listing frame: 58 bytes - 0000: 00 00 00 36 02 00 00 00 - 0008: 51 0d cf 9a 1c f8 9f 02 - 0010: 0b be aa fc f3 7b fd be - 0018: 1e ce ab 3a 1c 0b 73 f9 - 0020: 02 be 6a fa 3e d7 e9 92 - 0028: a6 0f d6 dc a6 17 60 b8 - 0030: 50 5e b3 06 0b e1 d8 ab - 0038: 60 14 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 +listing frame: 54 bytes + 0000: 00 00 00 32 7c f5 3f 94 + 0008: 77 30 97 88 c6 ed 88 34 + 0010: 00 8f 39 4a 3b e5 f2 12 + 0018: 95 9f ac 55 fa 9a cc a7 + 0020: fd 1f e9 57 c5 26 7c e1 + 0028: 59 c8 f3 fe 95 bc 4f 49 + 0030: 6f 6b 2f b5 fa 39 Responder stream 0 (height 31), epoch 0 frame 0: Supply(Continue) supply run: 1 record(s) - record 0: version 1, message 9 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a a8 05 00 00 00 68 65 - 0010: 6c 6c 6f + record 0: version 2, message 6 byte(s) + 0000: 66 00 00 00 0c 00 00 00 + 0008: 08 41 b8 65 77 6f 72 6c + 0010: 64 frame 1: Supply(End) supply run: 1 record(s) - record 0: version 2, message 9 byte(s) - 0000: 77 00 00 00 0e 00 00 00 - 0008: 0a b8 05 00 00 00 77 6f - 0010: 72 6c 64 + record 0: version 1, message 6 byte(s) + 0000: 77 00 00 00 0c 00 00 00 + 0008: 08 41 a8 65 68 65 6c 6c + 0010: 6f frame 2: End(Stream) 0000: 99 trailing frame: 6 bytes @@ -59,7 +58,7 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 1 bytes 0000: 2e diff --git a/tests/snapshots/bootstrap_snapshot__v1_populated_provider.snap b/tests/snapshots/bootstrap_snapshot__v1_populated_provider.snap index 4ae644ee1..3d10e73f6 100644 --- a/tests/snapshots/bootstrap_snapshot__v1_populated_provider.snap +++ b/tests/snapshots/bootstrap_snapshot__v1_populated_provider.snap @@ -14,45 +14,43 @@ received 25 bytes │ received 25 bytes 0008: 00 00 00 00 00 00 00 00 │ 0008: 8c 18 96 68 c9 e4 83 72 0010: 00 00 00 00 00 00 00 00 │ 0010: 37 bf 31 e0 2d 7f 6b 70 0018: 00 │ 0018: 00 -sent 5 bytes │ sent 5 bytes - 0000: 00 00 00 01 92 │ 0000: 00 00 00 01 e0 -received 13 bytes │ received 5 bytes - 0000: 00 00 00 01 e0 00 00 00 │ 0000: 00 00 00 01 92 - 0008: 04 00 00 00 00 │ sent 8 bytes +sent 6 bytes │ sent 6 bytes + 0000: 00 00 00 02 41 92 │ 0000: 00 00 00 02 41 e0 +received 14 bytes │ received 6 bytes + 0000: 00 00 00 02 41 e0 00 00 │ 0000: 00 00 00 02 41 92 + 0008: 00 04 00 00 00 00 │ sent 8 bytes sent 83 bytes │ 0000: 00 00 00 04 00 00 00 00 0000: 00 00 00 4f 03 00 00 00 │ received 83 bytes - 0008: 09 7c a8 b0 7c b2 82 54 │ 0000: 00 00 00 4f 03 00 00 00 - 0010: 10 9e b3 f8 96 75 cd b2 │ 0008: 09 7c a8 b0 7c b2 82 54 - 0018: 2a b6 88 2b 6a b3 1b 3c │ 0010: 10 9e b3 f8 96 75 cd b2 - 0020: df a0 b7 e9 9b 38 1b a2 │ 0018: 2a b6 88 2b 6a b3 1b 3c - 0028: cb 4d 2f 4f ef 62 4e d0 │ 0020: df a0 b7 e9 9b 38 1b a2 - 0030: 66 74 d9 e0 a3 67 d0 52 │ 0028: cb 4d 2f 4f ef 62 4e d0 - 0038: 72 d6 c4 bf a8 f5 9b 4a │ 0030: 66 74 d9 e0 a3 67 d0 52 - 0040: 41 4e 53 35 b1 c4 a3 23 │ 0038: 72 d6 c4 bf a8 f5 9b 4a - 0048: b9 0c 6f a7 52 9b cf 7c │ 0040: 41 4e 53 35 b1 c4 a3 23 - 0050: a5 82 16 │ 0048: b9 0c 6f a7 52 9b cf 7c -received 19 bytes │ 0050: a5 82 16 + 0008: 7c f5 3f 94 77 30 97 88 │ 0000: 00 00 00 4f 03 00 00 00 + 0010: c6 ed 88 34 00 8f 39 4a │ 0008: 7c f5 3f 94 77 30 97 88 + 0018: 3b e5 f2 12 95 9f ac 55 │ 0010: c6 ed 88 34 00 8f 39 4a + 0020: fa 88 cf 55 a1 9b 32 0f │ 0018: 3b e5 f2 12 95 9f ac 55 + 0028: ee 28 bb dd 59 98 ca bc │ 0020: fa 88 cf 55 a1 9b 32 0f + 0030: 6c 00 00 ca eb 8c 56 9a │ 0028: ee 28 bb dd 59 98 ca bc + 0038: d9 75 9a cc a7 fd 1f e9 │ 0030: 6c 00 00 ca eb 8c 56 9a + 0040: 57 c5 26 7c e1 59 c8 f3 │ 0038: d9 75 9a cc a7 fd 1f e9 + 0048: fe 95 bc 4f 49 6f 6b 2f │ 0040: 57 c5 26 7c e1 59 c8 f3 + 0050: b5 fa 39 │ 0048: fe 95 bc 4f 49 6f 6b 2f +received 19 bytes │ 0050: b5 fa 39 0000: 00 00 00 0f 00 00 00 00 │ sent 19 bytes - 0008: 03 00 00 00 09 a0 c4 00 │ 0000: 00 00 00 0f 00 00 00 00 - 0010: 00 00 00 │ 0008: 03 00 00 00 09 a0 c4 00 -sent 147 bytes │ 0010: 00 00 00 - 0000: 00 00 00 8a 03 00 00 00 │ received 147 bytes - 0008: 09 a7 1e 00 8c 87 d8 5a │ 0000: 00 00 00 8a 03 00 00 00 - 0010: bd 97 aa 1c 42 c8 69 6e │ 0008: 09 a7 1e 00 8c 87 d8 5a - 0018: d8 cc 4c 63 6d 30 7e 54 │ 0010: bd 97 aa 1c 42 c8 69 6e - 0020: 99 86 d2 49 2c fa bf 87 │ 0018: d8 cc 4c 63 6d 30 7e 54 - 0028: 70 a8 01 00 00 00 00 00 │ 0020: 99 86 d2 49 2c fa bf 87 - 0030: 00 00 a0 bf 1e e4 4b a4 │ 0028: 70 a8 01 00 00 00 00 00 - 0038: 65 86 2a 5d 6f f9 1c e0 │ 0030: 00 00 a0 bf 1e e4 4b a4 - 0040: e3 b8 e0 57 06 a1 ca 17 │ 0038: 65 86 2a 5d 6f f9 1c e0 - 0048: be 7f 37 2f 64 7e c5 9d │ 0040: e3 b8 e0 57 06 a1 ca 17 - 0050: 04 dd 25 b8 02 00 00 00 │ 0048: be 7f 37 2f 64 7e c5 9d - 0058: 00 00 00 00 c4 a3 1e 0d │ 0050: 04 dd 25 b8 02 00 00 00 - 0060: c3 e0 7d 4f ee a4 12 3e │ 0058: 00 00 00 00 c4 a3 1e 0d - 0068: 57 c0 b4 0f bb ab 29 46 │ 0060: c3 e0 7d 4f ee a4 12 3e - 0070: d7 9c 05 f2 17 df d2 68 │ 0068: 57 c0 b4 0f bb ab 29 46 - 0078: 95 5b 6a fa 3f 92 03 00 │ 0070: d7 9c 05 f2 17 df d2 68 - 0080: 00 00 00 00 00 00 00 00 │ 0078: 95 5b 6a fa 3f 92 03 00 - 0088: 00 00 00 00 00 00 00 00 │ 0080: 00 00 00 00 00 00 00 00 - 0090: 00 01 48 │ 0088: 00 00 00 00 00 00 00 00 - │ 0090: 00 01 48 + 0008: 03 00 00 00 7c 88 9a 00 │ 0000: 00 00 00 0f 00 00 00 00 + 0010: 00 00 00 │ 0008: 03 00 00 00 7c 88 9a 00 +sent 132 bytes │ 0010: 00 00 00 + 0000: 00 00 00 7b 03 00 00 00 │ received 132 bytes + 0008: 7c 7e 1e ac 70 77 80 ee │ 0000: 00 00 00 7b 03 00 00 00 + 0010: ae 6a 3c 6c 38 8e a7 c7 │ 0008: 7c 7e 1e ac 70 77 80 ee + 0018: 5d 41 2d 7a 9a a5 07 25 │ 0010: ae 6a 3c 6c 38 8e a7 c7 + 0020: c3 c6 72 cc ab 74 a8 ea │ 0018: 5d 41 2d 7a 9a a5 07 25 + 0028: 25 41 b8 41 02 88 a3 1e │ 0020: c3 c6 72 cc ab 74 a8 ea + 0030: 06 2b 4e 7a bf 45 fe d2 │ 0028: 25 41 b8 41 02 88 a3 1e + 0038: 3a 6f fe a0 36 d1 26 0d │ 0030: 06 2b 4e 7a bf 45 fe d2 + 0040: 77 f3 3a ae 0e 34 02 86 │ 0038: 3a 6f fe a0 36 d1 26 0d + 0048: ef af 9e 2e b5 81 41 92 │ 0040: 77 f3 3a ae 0e 34 02 86 + 0050: 41 03 9a da 1e e1 96 14 │ 0048: ef af 9e 2e b5 81 41 92 + 0058: 75 1b b9 ad 73 2f 4b 21 │ 0050: 41 03 9a da 1e e1 96 14 + 0060: ca a1 94 1f f5 85 97 bf │ 0058: 75 1b b9 ad 73 2f 4b 21 + 0068: f8 44 76 9f 71 70 7b de │ 0060: ca a1 94 1f f5 85 97 bf + 0070: 77 f0 e9 41 a8 41 01 00 │ 0068: f8 44 76 9f 71 70 7b de + 0078: 00 00 00 00 00 00 00 00 │ 0070: 77 f0 e9 41 a8 41 01 00 + 0080: 00 00 01 48 │ 0078: 00 00 00 00 00 00 00 00 + │ 0080: 00 00 01 48 diff --git a/tests/snapshots/gossip_snapshot__asymmetric_message_targets_unbatch_the_run.snap b/tests/snapshots/gossip_snapshot__asymmetric_message_targets_unbatch_the_run.snap index 5ed038a4e..b1dffb791 100644 --- a/tests/snapshots/gossip_snapshot__asymmetric_message_targets_unbatch_the_run.snap +++ b/tests/snapshots/gossip_snapshot__asymmetric_message_targets_unbatch_the_run.snap @@ -8,34 +8,32 @@ preamble: 25 bytes 0008: 8c 18 96 68 c9 e4 83 72 0010: 37 bf 31 e0 2d 7f 6b 70 0018: 00 -version: (0, 2, 0) -greeting words: set len 2, version-size bound 2, message-size target 1638912 -version frame: 30 bytes - 0000: 00 00 00 1a 02 00 00 00 - 0008: 00 00 00 00 02 00 00 00 +version: (0, 2046, 0) +greeting words: set len 2, version-size bound 3, message-size target 1638912 +version frame: 34 bytes + 0000: 00 00 00 1e 02 00 00 00 + 0008: 00 00 00 00 03 00 00 00 0010: 00 00 00 00 00 02 19 00 - 0018: 00 00 00 00 5c 90 + 0018: 00 00 00 00 40 0f ff 00 + 0020: 1f f9 listing: 1 child(ren) - child 0x71: d02b23197dc11ce40f401e66b46b11040502f40d37ce905d -listing frame: 33 bytes - 0000: 00 00 00 1d 01 00 00 00 - 0008: 71 d0 2b 23 19 7d c1 1c - 0010: e4 0f 40 1e 66 b4 6b 11 - 0018: 04 05 02 f4 0d 37 ce 90 - 0020: 5d + child 0x79: 5f06438950116f9d1463d15d1c3b1dadd5e17dcd5e244f29 +listing frame: 29 bytes + 0000: 00 00 00 19 79 5f 06 43 + 0008: 89 50 11 6f 9d 14 63 d1 + 0010: 5d 1c 3b 1d ad d5 e1 7d + 0018: cd 5e 24 4f 29 Responder stream 0 (height 31), epoch 0 frame 0: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 2, 0), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 5c 90 52 6c 00 00 00 - 0010: 00 00 00 + record 0: version (0, 26, 0), message 2 byte(s) + 0000: 66 00 00 00 0a 00 00 00 + 0008: 06 43 43 70 69 18 19 frame 1: Supply(End) supply run: 1 record(s) - record 0: version (0, 1, 0), message 8 byte(s) - 0000: 77 00 00 00 0e 00 00 00 - 0008: 0a 55 40 01 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 20, 0), message 1 byte(s) + 0000: 77 00 00 00 09 00 00 00 + 0008: 05 43 42 b0 51 13 frame 2: End(Stream) 0000: 99 trailing frame: 1 bytes @@ -55,7 +53,7 @@ version frame: 29 bytes 0010: 00 00 00 00 00 00 00 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 1 bytes 0000: 2e diff --git a/tests/snapshots/gossip_snapshot__batched_supply_run.snap b/tests/snapshots/gossip_snapshot__batched_supply_run.snap index 569930066..ad1e0f7ac 100644 --- a/tests/snapshots/gossip_snapshot__batched_supply_run.snap +++ b/tests/snapshots/gossip_snapshot__batched_supply_run.snap @@ -8,31 +8,29 @@ preamble: 25 bytes 0008: 8c 18 96 68 c9 e4 83 72 0010: 37 bf 31 e0 2d 7f 6b 70 0018: 00 -version: (0, 2, 0) -greeting words: set len 2, version-size bound 2, message-size target 1638912 -version frame: 30 bytes - 0000: 00 00 00 1a 02 00 00 00 - 0008: 00 00 00 00 02 00 00 00 +version: (0, 2046, 0) +greeting words: set len 2, version-size bound 3, message-size target 1638912 +version frame: 34 bytes + 0000: 00 00 00 1e 02 00 00 00 + 0008: 00 00 00 00 03 00 00 00 0010: 00 00 00 00 00 02 19 00 - 0018: 00 00 00 00 5c 90 + 0018: 00 00 00 00 40 0f ff 00 + 0020: 1f f9 listing: 1 child(ren) - child 0x71: d02b23197dc11ce40f401e66b46b11040502f40d37ce905d -listing frame: 33 bytes - 0000: 00 00 00 1d 01 00 00 00 - 0008: 71 d0 2b 23 19 7d c1 1c - 0010: e4 0f 40 1e 66 b4 6b 11 - 0018: 04 05 02 f4 0d 37 ce 90 - 0020: 5d + child 0x79: 5f06438950116f9d1463d15d1c3b1dadd5e17dcd5e244f29 +listing frame: 29 bytes + 0000: 00 00 00 19 79 5f 06 43 + 0008: 89 50 11 6f 9d 14 63 d1 + 0010: 5d 1c 3b 1d ad d5 e1 7d + 0018: cd 5e 24 4f 29 Responder stream 0 (height 31), epoch 0 frame 0: Supply(End) supply run: 2 record(s) - record 0: version (0, 2, 0), message 8 byte(s) - record 1: version (0, 1, 0), message 8 byte(s) - 0000: 77 00 00 00 1c 00 00 00 - 0008: 0a 5c 90 52 6c 00 00 00 - 0010: 00 00 00 00 00 00 0a 55 - 0018: 40 01 00 00 00 00 00 00 - 0020: 00 + record 0: version (0, 26, 0), message 2 byte(s) + record 1: version (0, 20, 0), message 1 byte(s) + 0000: 77 00 00 00 13 00 00 00 + 0008: 06 43 43 70 69 18 19 00 + 0010: 00 00 05 43 42 b0 51 13 frame 1: End(Stream) 0000: 99 trailing frame: 1 bytes @@ -52,7 +50,7 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 1 bytes 0000: 2e diff --git a/tests/snapshots/gossip_snapshot__both_redact_the_same_message.snap b/tests/snapshots/gossip_snapshot__both_redact_the_same_message.snap new file mode 100644 index 000000000..e9d20c958 --- /dev/null +++ b/tests/snapshots/gossip_snapshot__both_redact_the_same_message.snap @@ -0,0 +1,54 @@ +--- +source: tests/gossip_snapshot.rs +expression: "capture_gossip(a, b)" +--- +direction A -> B +preamble: 25 bytes + 0000: 52 55 4d 4f 52 53 00 02 + 0008: 8c 18 96 68 c9 e4 83 72 + 0010: 37 bf 31 e0 2d 7f 6b 70 + 0018: 00 +version: (2, 1, 0) +greeting words: set len 1, version-size bound 1, message-size target 1638912 +version frame: 30 bytes + 0000: 00 00 00 1a 01 00 00 00 + 0008: 00 00 00 00 01 00 00 00 + 0010: 00 00 00 00 00 02 19 00 + 0018: 00 00 00 00 49 50 +listing: 1 child(ren) + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa +listing frame: 29 bytes + 0000: 00 00 00 19 7c f5 3f 94 + 0008: 77 30 97 88 c6 ed 88 34 + 0010: 00 8f 39 4a 3b e5 f2 12 + 0018: 95 9f ac 55 fa +Responder stream 0 (height 31), epoch 0 + frame 0: Match(End) + 0000: 11 + frame 1: End(Stream) + 0000: 99 +trailing frame: 1 bytes + 0000: 2e + +direction B -> A +preamble: 25 bytes + 0000: 52 55 4d 4f 52 53 00 02 + 0008: 8c 18 96 68 c9 e4 83 72 + 0010: 37 bf 31 e0 2d 7f 6b 70 + 0018: 00 +version: (2, 0, 1) +greeting words: set len 1, version-size bound 1, message-size target 1638912 +version frame: 30 bytes + 0000: 00 00 00 1a 01 00 00 00 + 0008: 00 00 00 00 01 00 00 00 + 0010: 00 00 00 00 00 02 19 00 + 0018: 00 00 00 00 5d c0 +listing: 1 child(ren) + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa +listing frame: 29 bytes + 0000: 00 00 00 19 7c f5 3f 94 + 0008: 77 30 97 88 c6 ed 88 34 + 0010: 00 8f 39 4a 3b e5 f2 12 + 0018: 95 9f ac 55 fa +trailing frame: 1 bytes + 0000: 2e diff --git a/tests/snapshots/gossip_snapshot__bulk_initiator_ships_opening_supplies.snap b/tests/snapshots/gossip_snapshot__bulk_initiator_ships_opening_supplies.snap index 066585c08..c240c319b 100644 --- a/tests/snapshots/gossip_snapshot__bulk_initiator_ships_opening_supplies.snap +++ b/tests/snapshots/gossip_snapshot__bulk_initiator_ships_opening_supplies.snap @@ -8,31 +8,28 @@ preamble: 25 bytes 0008: 8c 18 96 68 c9 e4 83 72 0010: 37 bf 31 e0 2d 7f 6b 70 0018: 00 -version: (0, 2, 0) -greeting words: set len 2, version-size bound 2, message-size target 1638912 -version frame: 30 bytes - 0000: 00 00 00 1a 02 00 00 00 - 0008: 00 00 00 00 02 00 00 00 +version: (0, 126, 0) +greeting words: set len 2, version-size bound 4, message-size target 1638912 +version frame: 32 bytes + 0000: 00 00 00 1c 02 00 00 00 + 0008: 00 00 00 00 04 00 00 00 0010: 00 00 00 00 00 02 19 00 - 0018: 00 00 00 00 5c 90 + 0018: 00 00 00 00 40 ff 01 f9 listing: 1 child(ren) - child 0x71: c22fb6b572126e113c6e2ec3ac068d1d8d06a292391a535d -listing frame: 33 bytes - 0000: 00 00 00 1d 01 00 00 00 - 0008: 71 c2 2f b6 b5 72 12 6e - 0010: 11 3c 6e 2e c3 ac 06 8d - 0018: 1d 8d 06 a2 92 39 1a 53 - 0020: 5d + child 0x1b: 4c9f80ab8bdeed98a72d4976c2879ccd5cb17e2aeacd24d3 +listing frame: 29 bytes + 0000: 00 00 00 19 1b 4c 9f 80 + 0008: ab 8b de ed 98 a7 2d 49 + 0010: 76 c2 87 9c cd 5c b1 7e + 0018: 2a ea cd 24 d3 Initiator stream 0 (height 31), epoch 0 frame 0: Supply(End) supply run: 2 record(s) - record 0: version (0, 1, 0), message 8 byte(s) - record 1: version (0, 2, 0), message 8 byte(s) - 0000: 77 00 00 00 1c 00 00 00 - 0008: 0a 55 40 01 00 00 00 00 - 0010: 00 00 00 00 00 00 0a 5c - 0018: 90 1f 01 00 00 00 00 00 - 0020: 00 + record 0: version (0, 31, 0), message 2 byte(s) + record 1: version (0, 4, 0), message 1 byte(s) + 0000: 77 00 00 00 13 00 00 00 + 0008: 07 44 41 04 1f 40 18 1e + 0010: 00 00 00 04 42 4b 11 03 frame 1: End(Stream) 0000: 99 Initiator stream 1 (height 30), epoch 0 @@ -49,50 +46,46 @@ preamble: 25 bytes 0008: 8c 18 96 68 c9 e4 83 72 0010: 37 bf 31 e0 2d 7f 6b 70 0018: 00 -version: (0, 0, 3) +version: (0, 0, 29) greeting words: set len 3, version-size bound 2, message-size target 1638912 version frame: 30 bytes 0000: 00 00 00 1a 03 00 00 00 0008: 00 00 00 00 02 00 00 00 0010: 00 00 00 00 00 02 19 00 - 0018: 00 00 00 00 73 c0 + 0018: 00 00 00 00 70 77 listing: 3 child(ren) - child 0x1e: 1696ea4f18588296856f27cfda68cb1f105f515fb9af8201 - child 0x6a: 777d5b030dc38261c051534243674f5a3e47610a7d18e223 - child 0xf6: df6854dada8919279a624fb24ac491c31762c3e530f88d5d -listing frame: 83 bytes - 0000: 00 00 00 4f 03 00 00 00 - 0008: 1e 16 96 ea 4f 18 58 82 - 0010: 96 85 6f 27 cf da 68 cb - 0018: 1f 10 5f 51 5f b9 af 82 - 0020: 01 6a 77 7d 5b 03 0d c3 - 0028: 82 61 c0 51 53 42 43 67 - 0030: 4f 5a 3e 47 61 0a 7d 18 - 0038: e2 23 f6 df 68 54 da da - 0040: 89 19 27 9a 62 4f b2 4a - 0048: c4 91 c3 17 62 c3 e5 30 - 0050: f8 8d 5d + child 0x1a: f1f3aa8a784d1361fc809d57df14a1955f8956c55e120e19 + child 0x36: a93c14a59c62f37321d21557b0e821de471a7e3d079c6fce + child 0xf2: 42bb8c7ce00cc0681c05edde50af1bd47d5d94d0a136c882 +listing frame: 79 bytes + 0000: 00 00 00 4b 1a f1 f3 aa + 0008: 8a 78 4d 13 61 fc 80 9d + 0010: 57 df 14 a1 95 5f 89 56 + 0018: c5 5e 12 0e 19 36 a9 3c + 0020: 14 a5 9c 62 f3 73 21 d2 + 0028: 15 57 b0 e8 21 de 47 1a + 0030: 7e 3d 07 9c 6f ce f2 42 + 0038: bb 8c 7c e0 0c c0 68 1c + 0040: 05 ed de 50 af 1b d4 7d + 0048: 5d 94 d0 a1 36 c8 82 Responder stream 0 (height 31), epoch 0 frame 0: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 2), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 72 c0 65 00 00 00 00 - 0010: 00 00 00 - frame 1: Supply(Continue) - supply run: 1 record(s) - record 0: version (0, 0, 3), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 73 c0 66 00 00 00 00 - 0010: 00 00 00 - frame 2: QueryEmpty(Continue) + record 0: version (0, 0, 3), message 3 byte(s) + 0000: 66 00 00 00 0a 00 00 00 + 0008: 06 42 73 c0 19 27 12 + frame 1: QueryEmpty(Continue) 0000: 22 + frame 2: Supply(Continue) + supply run: 1 record(s) + record 0: version (0, 0, 2), message 3 byte(s) + 0000: 66 00 00 00 0a 00 00 00 + 0008: 06 42 72 c0 19 27 11 frame 3: Supply(End) supply run: 1 record(s) - record 0: version (0, 0, 1), message 8 byte(s) - 0000: 77 00 00 00 0d 00 00 00 - 0008: 09 77 64 00 00 00 00 00 - 0010: 00 00 + record 0: version (0, 0, 1), message 3 byte(s) + 0000: 77 00 00 00 09 00 00 00 + 0008: 05 41 77 19 27 10 frame 4: End(Stream) 0000: 99 trailing frame: 1 bytes diff --git a/tests/snapshots/gossip_snapshot__converged_forks_noop.snap b/tests/snapshots/gossip_snapshot__converged_forks_noop.snap index 99dcfd34f..efa28d7bc 100644 --- a/tests/snapshots/gossip_snapshot__converged_forks_noop.snap +++ b/tests/snapshots/gossip_snapshot__converged_forks_noop.snap @@ -16,17 +16,16 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 b8 listing: 2 child(ren) - child 0x9: 7ca8b07cb28254109eb3f89675cdb22ab6882b6ab31b3cdf - child 0xa0: b7e99b381ba2cb4d2f4fef624ed06674d9e0a367d05272d6 -listing frame: 58 bytes - 0000: 00 00 00 36 02 00 00 00 - 0008: 09 7c a8 b0 7c b2 82 54 - 0010: 10 9e b3 f8 96 75 cd b2 - 0018: 2a b6 88 2b 6a b3 1b 3c - 0020: df a0 b7 e9 9b 38 1b a2 - 0028: cb 4d 2f 4f ef 62 4e d0 - 0030: 66 74 d9 e0 a3 67 d0 52 - 0038: 72 d6 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 +listing frame: 54 bytes + 0000: 00 00 00 32 7c f5 3f 94 + 0008: 77 30 97 88 c6 ed 88 34 + 0010: 00 8f 39 4a 3b e5 f2 12 + 0018: 95 9f ac 55 fa 9a cc a7 + 0020: fd 1f e9 57 c5 26 7c e1 + 0028: 59 c8 f3 fe 95 bc 4f 49 + 0030: 6f 6b 2f b5 fa 39 trailing frame: 1 bytes 0000: 2e @@ -44,16 +43,15 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 b8 listing: 2 child(ren) - child 0x9: 7ca8b07cb28254109eb3f89675cdb22ab6882b6ab31b3cdf - child 0xa0: b7e99b381ba2cb4d2f4fef624ed06674d9e0a367d05272d6 -listing frame: 58 bytes - 0000: 00 00 00 36 02 00 00 00 - 0008: 09 7c a8 b0 7c b2 82 54 - 0010: 10 9e b3 f8 96 75 cd b2 - 0018: 2a b6 88 2b 6a b3 1b 3c - 0020: df a0 b7 e9 9b 38 1b a2 - 0028: cb 4d 2f 4f ef 62 4e d0 - 0030: 66 74 d9 e0 a3 67 d0 52 - 0038: 72 d6 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 +listing frame: 54 bytes + 0000: 00 00 00 32 7c f5 3f 94 + 0008: 77 30 97 88 c6 ed 88 34 + 0010: 00 8f 39 4a 3b e5 f2 12 + 0018: 95 9f ac 55 fa 9a cc a7 + 0020: fd 1f e9 57 c5 26 7c e1 + 0028: 59 c8 f3 fe 95 bc 4f 49 + 0030: 6f 6b 2f b5 fa 39 trailing frame: 1 bytes 0000: 2e diff --git a/tests/snapshots/gossip_snapshot__deep_trie_divergence.snap b/tests/snapshots/gossip_snapshot__deep_trie_divergence.snap index 162a7266f..d9ef1cc65 100644 --- a/tests/snapshots/gossip_snapshot__deep_trie_divergence.snap +++ b/tests/snapshots/gossip_snapshot__deep_trie_divergence.snap @@ -16,213 +16,187 @@ version frame: 31 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 42 30 41 listing: 16 child(ren) - child 0xb: fa4e0fde3cff1ec425ae47e5d58693cec2783afa0d3df900 - child 0x13: 04f7cb9e4209c2cca529dd6e3514749857641a70ce0a4fda - child 0x30: ae035fe7f2adf3909decdbdbd0a12a1807920456bb24c0e4 - child 0x33: bb196fa72d3e93ba55efa0a222e0fdec146ff11d149683cc - child 0x3c: 51d0a4c538e0ddc48ef14a84f8be8674e7a5b8b14d7a288f - child 0x4d: 16406d769552a0fb82dfff2f1d9d912261997bb4ba7cf1be - child 0x5b: ecb1d4d8612b9150359cca6be000290b773d7a4be8d8fc32 - child 0x6b: 8227176e86e0496aa3fc972a0d9cddfcd9990d55e91d564a - child 0x96: 0361ac491501682664d81c480609cf3bc25cd14b1ccc9883 - child 0xa6: 96a4bbb0c3c7adc345d08720f589439794c78596ce0436be - child 0xb2: 7bf9dc666b822f059f1ec74099f81c71be55f32517bd5679 - child 0xce: 6e1c80853d07465e3dc1bb7501622b00759e8c912e617b39 - child 0xdb: c9f670c445178415edea177b730c9e64d49ba6c3d75faea4 - child 0xf1: 6f20e6a4bd7e56f74d1cbd07c10487e26f1b99fd2cf4c397 - child 0xfc: 1acb40cb447668ad0001946d97cd15da1207117b5c114bd2 - child 0xfd: 512434fa98c0eeb26e52f7adfde4c8a9e9b47c12c473a64d -listing frame: 408 bytes - 0000: 00 00 01 94 10 00 00 00 - 0008: 0b fa 4e 0f de 3c ff 1e - 0010: c4 25 ae 47 e5 d5 86 93 - 0018: ce c2 78 3a fa 0d 3d f9 - 0020: 00 13 04 f7 cb 9e 42 09 - 0028: c2 cc a5 29 dd 6e 35 14 - 0030: 74 98 57 64 1a 70 ce 0a - 0038: 4f da 30 ae 03 5f e7 f2 - 0040: ad f3 90 9d ec db db d0 - 0048: a1 2a 18 07 92 04 56 bb - 0050: 24 c0 e4 33 bb 19 6f a7 - 0058: 2d 3e 93 ba 55 ef a0 a2 - 0060: 22 e0 fd ec 14 6f f1 1d - 0068: 14 96 83 cc 3c 51 d0 a4 - 0070: c5 38 e0 dd c4 8e f1 4a - 0078: 84 f8 be 86 74 e7 a5 b8 - 0080: b1 4d 7a 28 8f 4d 16 40 - 0088: 6d 76 95 52 a0 fb 82 df - 0090: ff 2f 1d 9d 91 22 61 99 - 0098: 7b b4 ba 7c f1 be 5b ec - 00a0: b1 d4 d8 61 2b 91 50 35 - 00a8: 9c ca 6b e0 00 29 0b 77 - 00b0: 3d 7a 4b e8 d8 fc 32 6b - 00b8: 82 27 17 6e 86 e0 49 6a - 00c0: a3 fc 97 2a 0d 9c dd fc - 00c8: d9 99 0d 55 e9 1d 56 4a - 00d0: 96 03 61 ac 49 15 01 68 - 00d8: 26 64 d8 1c 48 06 09 cf - 00e0: 3b c2 5c d1 4b 1c cc 98 - 00e8: 83 a6 96 a4 bb b0 c3 c7 - 00f0: ad c3 45 d0 87 20 f5 89 - 00f8: 43 97 94 c7 85 96 ce 04 - 0100: 36 be b2 7b f9 dc 66 6b - 0108: 82 2f 05 9f 1e c7 40 99 - 0110: f8 1c 71 be 55 f3 25 17 - 0118: bd 56 79 ce 6e 1c 80 85 - 0120: 3d 07 46 5e 3d c1 bb 75 - 0128: 01 62 2b 00 75 9e 8c 91 - 0130: 2e 61 7b 39 db c9 f6 70 - 0138: c4 45 17 84 15 ed ea 17 - 0140: 7b 73 0c 9e 64 d4 9b a6 - 0148: c3 d7 5f ae a4 f1 6f 20 - 0150: e6 a4 bd 7e 56 f7 4d 1c - 0158: bd 07 c1 04 87 e2 6f 1b - 0160: 99 fd 2c f4 c3 97 fc 1a - 0168: cb 40 cb 44 76 68 ad 00 - 0170: 01 94 6d 97 cd 15 da 12 - 0178: 07 11 7b 5c 11 4b d2 fd - 0180: 51 24 34 fa 98 c0 ee b2 - 0188: 6e 52 f7 ad fd e4 c8 a9 - 0190: e9 b4 7c 12 c4 73 a6 4d + child 0x4: 4d08cfcd4cf728cc13583332b6cd0c76cfe4e2498224a962 + child 0x1b: 1ea6c1c25041537b9a1602794a36d951f44fbdc7edb3e398 + child 0x21: 6bdfff4b8f9e7c9a8987b5b91c63112dd13386cb1755eb52 + child 0x4b: accae91486823c7c298ecf001e14dcb265380be95fd27e07 + child 0x66: 404620be25055e0824849dcbf40c8e88002ef4c4b3a19832 + child 0x6c: 8460519a71f03836e27029e150e6a45777a5b6c989cd6707 + child 0x6f: aa7dff928791534d97c9a14867af2d4aec7dc9f35930e6ed + child 0x87: 55a3d6508c651d3b0e5c4d5f5415ac5136c1f1a8eeb84de2 + child 0x93: c4c0139f3f4f4afe9715d26a43953691cdc861574ffc8196 + child 0xa0: 7835b0a8791af76763e8248004d356e7d5c938bb893ecc9c + child 0xb4: 4ac23f4c5f0f4ab2b981488bbef7332ff5038ae1a500dac5 + child 0xb7: 0c6e8d58c7dc2afc324e6323f7597f91edc316c06d70eae3 + child 0xc4: cbd073bca9b510a182ce1abf0c260bceeaf630e1dbfc64da + child 0xd0: 16470168f5ac21cd9435e6c52d1bd47e79b1fed68a845781 + child 0xf3: 7a295e6ea121ff22d3df5eeed5799451fca896836b7f18ce + child 0xf5: 1d9c1fa1c842dd2eac5b8f5773f06978dc1276e2f86b8ae8 +listing frame: 404 bytes + 0000: 00 00 01 90 04 4d 08 cf + 0008: cd 4c f7 28 cc 13 58 33 + 0010: 32 b6 cd 0c 76 cf e4 e2 + 0018: 49 82 24 a9 62 1b 1e a6 + 0020: c1 c2 50 41 53 7b 9a 16 + 0028: 02 79 4a 36 d9 51 f4 4f + 0030: bd c7 ed b3 e3 98 21 6b + 0038: df ff 4b 8f 9e 7c 9a 89 + 0040: 87 b5 b9 1c 63 11 2d d1 + 0048: 33 86 cb 17 55 eb 52 4b + 0050: ac ca e9 14 86 82 3c 7c + 0058: 29 8e cf 00 1e 14 dc b2 + 0060: 65 38 0b e9 5f d2 7e 07 + 0068: 66 40 46 20 be 25 05 5e + 0070: 08 24 84 9d cb f4 0c 8e + 0078: 88 00 2e f4 c4 b3 a1 98 + 0080: 32 6c 84 60 51 9a 71 f0 + 0088: 38 36 e2 70 29 e1 50 e6 + 0090: a4 57 77 a5 b6 c9 89 cd + 0098: 67 07 6f aa 7d ff 92 87 + 00a0: 91 53 4d 97 c9 a1 48 67 + 00a8: af 2d 4a ec 7d c9 f3 59 + 00b0: 30 e6 ed 87 55 a3 d6 50 + 00b8: 8c 65 1d 3b 0e 5c 4d 5f + 00c0: 54 15 ac 51 36 c1 f1 a8 + 00c8: ee b8 4d e2 93 c4 c0 13 + 00d0: 9f 3f 4f 4a fe 97 15 d2 + 00d8: 6a 43 95 36 91 cd c8 61 + 00e0: 57 4f fc 81 96 a0 78 35 + 00e8: b0 a8 79 1a f7 67 63 e8 + 00f0: 24 80 04 d3 56 e7 d5 c9 + 00f8: 38 bb 89 3e cc 9c b4 4a + 0100: c2 3f 4c 5f 0f 4a b2 b9 + 0108: 81 48 8b be f7 33 2f f5 + 0110: 03 8a e1 a5 00 da c5 b7 + 0118: 0c 6e 8d 58 c7 dc 2a fc + 0120: 32 4e 63 23 f7 59 7f 91 + 0128: ed c3 16 c0 6d 70 ea e3 + 0130: c4 cb d0 73 bc a9 b5 10 + 0138: a1 82 ce 1a bf 0c 26 0b + 0140: ce ea f6 30 e1 db fc 64 + 0148: da d0 16 47 01 68 f5 ac + 0150: 21 cd 94 35 e6 c5 2d 1b + 0158: d4 7e 79 b1 fe d6 8a 84 + 0160: 57 81 f3 7a 29 5e 6e a1 + 0168: 21 ff 22 d3 df 5e ee d5 + 0170: 79 94 51 fc a8 96 83 6b + 0178: 7f 18 ce f5 1d 9c 1f a1 + 0180: c8 42 dd 2e ac 5b 8f 57 + 0188: 73 f0 69 78 dc 12 76 e2 + 0190: f8 6b 8a e8 Responder stream 0 (height 31), epoch 0 - frame 0: QueryEmpty(Continue) - 0000: 22 - frame 1: Supply(Continue) + frame 0: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 7, 0), message 8 byte(s) - 0000: 66 00 00 00 0f 00 00 00 - 0008: 0b 44 47 40 06 00 00 00 - 0010: 00 00 00 00 + record 0: version (0, 11, 0), message 1 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 43 46 42 d0 0a + frame 1: QueryEmpty(Continue) + 0000: 22 frame 2: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 16, 0), message 8 byte(s) - 0000: 66 00 00 00 0f 00 00 00 - 0008: 0b 42 30 41 0f 00 00 00 - 0010: 00 00 00 00 + record 0: version (0, 4, 0), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 4b 11 03 frame 3: QueryEmpty(Continue) 0000: 22 frame 4: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 13, 0), message 8 byte(s) - 0000: 66 00 00 00 0f 00 00 00 - 0008: 0b 47 43 50 0c 00 00 00 - 0010: 00 00 00 00 + record 0: version (0, 14, 0), message 1 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 43 47 c3 90 0d frame 5: QueryEmpty(Continue) 0000: 22 - frame 6: Supply(Continue) - supply run: 1 record(s) - record 0: version (0, 15, 0), message 8 byte(s) - 0000: 66 00 00 00 0f 00 00 00 - 0008: 0b 42 10 f4 0e 00 00 00 - 0010: 00 00 00 00 + frame 6: QueryEmpty(Continue) + 0000: 22 frame 7: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 9, 0), message 8 byte(s) - 0000: 66 00 00 00 0f 00 00 00 - 0008: 0b 45 42 50 08 00 00 00 - 0010: 00 00 00 00 + record 0: version (0, 13, 0), message 1 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 43 47 43 50 0c frame 8: QueryEmpty(Continue) 0000: 22 frame 9: QueryEmpty(Continue) 0000: 22 - frame 10: Supply(Continue) - supply run: 1 record(s) - record 0: version (0, 1, 0), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 55 40 00 00 00 00 00 - 0010: 00 00 00 - frame 11: QueryEmpty(Continue) + frame 10: QueryEmpty(Continue) 0000: 22 + frame 11: Supply(Continue) + supply run: 1 record(s) + record 0: version (0, 15, 0), message 1 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 43 42 10 f4 0e frame 12: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 5, 0), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 4d 15 04 00 00 00 00 - 0010: 00 00 00 - frame 13: QueryEmpty(Continue) + record 0: version (0, 16, 0), message 1 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 43 42 30 41 0f + frame 13: Supply(Continue) + supply run: 1 record(s) + record 0: version (0, 12, 0), message 1 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 43 46 c3 10 0b + frame 14: QueryEmpty(Continue) 0000: 22 - frame 14: Query(Continue) - query: 1 child(ren) - child 0xc8: 5a0314ab9ef16e3fbf2c4c1cdd4f7eeff3e91462d5d46f87 - 0000: 44 00 c8 5a 03 14 ab 9e - 0008: f1 6e 3f bf 2c 4c 1c dd - 0010: 4f 7e ef f3 e9 14 62 d5 - 0018: d4 6f 87 frame 15: QueryEmpty(Continue) 0000: 22 - frame 16: QueryEmpty(Continue) - 0000: 22 - frame 17: QueryEmpty(Continue) - 0000: 22 - frame 18: Supply(Continue) + frame 16: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 6, 0), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 4f 19 05 00 00 00 00 - 0010: 00 00 00 - frame 19: Supply(Continue) + record 0: version (0, 9, 0), message 1 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 43 45 42 50 08 + frame 17: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 2, 0), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 5c 90 01 00 00 00 00 - 0010: 00 00 00 - frame 20: QueryEmpty(Continue) + record 0: version (0, 1, 0), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 55 40 00 + frame 18: QueryEmpty(Continue) 0000: 22 + frame 19: Supply(Continue) + supply run: 1 record(s) + record 0: version (0, 2, 0), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 5c 90 01 + frame 20: Supply(Continue) + supply run: 1 record(s) + record 0: version (0, 8, 0), message 1 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 43 44 c2 10 07 frame 21: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 4, 0), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 4b 11 03 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 6, 0), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 4f 19 05 frame 22: QueryEmpty(Continue) 0000: 22 - frame 23: QueryEmpty(Continue) - 0000: 22 - frame 24: Supply(Continue) + frame 23: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 10, 0), message 8 byte(s) - 0000: 66 00 00 00 0f 00 00 00 - 0008: 0b 45 c2 90 09 00 00 00 - 0010: 00 00 00 00 - frame 25: QueryEmpty(Continue) + record 0: version (0, 10, 0), message 1 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 43 45 c2 90 09 + frame 24: QueryEmpty(Continue) 0000: 22 - frame 26: Supply(Continue) + frame 25: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 11, 0), message 8 byte(s) - 0000: 66 00 00 00 0f 00 00 00 - 0008: 0b 46 42 d0 0a 00 00 00 - 0010: 00 00 00 00 - frame 27: Supply(Continue) - supply run: 1 record(s) - record 0: version (0, 8, 0), message 8 byte(s) - 0000: 66 00 00 00 0f 00 00 00 - 0008: 0b 44 c2 10 07 00 00 00 - 0010: 00 00 00 00 - frame 28: QueryEmpty(Continue) + record 0: version (0, 3, 0), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 49 34 02 + frame 26: QueryEmpty(Continue) 0000: 22 - frame 29: Supply(Continue) + frame 27: QueryEmpty(Continue) + 0000: 22 + frame 28: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 12, 0), message 8 byte(s) - 0000: 66 00 00 00 0f 00 00 00 - 0008: 0b 46 c3 10 0b 00 00 00 - 0010: 00 00 00 00 - frame 30: Supply(End) + record 0: version (0, 7, 0), message 1 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 43 44 47 40 06 + frame 29: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 3, 0), message 8 byte(s) - 0000: 77 00 00 00 0e 00 00 00 - 0008: 0a 49 34 02 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 5, 0), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 4d 15 04 + frame 30: QueryEmpty(End) + 0000: 33 frame 31: End(Stream) 0000: 99 -Responder stream 1 (height 29), epoch 0 - frame 0: Supply(End) - supply run: 1 record(s) - record 0: version (0, 14, 0), message 8 byte(s) - 0000: 78 00 00 00 0f 00 00 00 - 0008: 0b 47 c3 90 0d 00 00 00 - 0010: 00 00 00 00 - frame 1: End(Stream) - 0000: 9a trailing frame: 1 bytes 0000: 2e @@ -239,166 +213,149 @@ version frame: 30 bytes 0008: 00 00 00 00 02 00 00 00 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 70 43 -listing: 16 child(ren) - child 0xa: e67eaf084bd05394633f8898e91fff8a8bf6366da09e0ae2 - child 0x2a: 0b3f926225aa5d31141544967fd3718461d135368ced83e3 - child 0x32: 7b66e9600be28e8e036524040169e51ff5d1dbdbe4c8a8eb - child 0x45: 35537aa226107447047fb28550a47284ae4a12dc3047b4ec - child 0x4b: eb1aa4629606250f5481c56c673733dd7771e3f531039a7e - child 0x4e: a50e8bc89daf89b1de995c203698cb1a69a8db5d718ceb7e - child 0x5e: 0fc6c96352e096e25770a4d3ccf3e8908a4603c171e374c6 - child 0x6b: 739882e4b7595b64238d381d334582d9b6117af1c705c737 - child 0x75: c8eed54b7669072cdb4b6fb929c2852257ccb41de1f0b300 - child 0x7e: 96aefedee3494a9598f729c69e8e5210c230278527058761 - child 0x83: c5c215aa3ad50d94df25e864660db6264f14acd284342d31 - child 0xad: 8ad4f0bc93eb20b87ea6ee416ba0fda4716c3686df83fa21 - child 0xb6: b78c4de87a2709f295377de37b4a9d74f37f4c12b7c02a18 - child 0xc6: eaf0ca393ac6bfc5d32d46a083ab61a4fae28aaedb0cc973 - child 0xd1: df8a4330326fcffa3002aed26a505de80da133e5388ebd8a - child 0xfa: 5b35a96ff9af19322aba6ee963b40571818d7b1b1f36e460 -listing frame: 408 bytes - 0000: 00 00 01 94 10 00 00 00 - 0008: 0a e6 7e af 08 4b d0 53 - 0010: 94 63 3f 88 98 e9 1f ff - 0018: 8a 8b f6 36 6d a0 9e 0a - 0020: e2 2a 0b 3f 92 62 25 aa - 0028: 5d 31 14 15 44 96 7f d3 - 0030: 71 84 61 d1 35 36 8c ed - 0038: 83 e3 32 7b 66 e9 60 0b - 0040: e2 8e 8e 03 65 24 04 01 - 0048: 69 e5 1f f5 d1 db db e4 - 0050: c8 a8 eb 45 35 53 7a a2 - 0058: 26 10 74 47 04 7f b2 85 - 0060: 50 a4 72 84 ae 4a 12 dc - 0068: 30 47 b4 ec 4b eb 1a a4 - 0070: 62 96 06 25 0f 54 81 c5 - 0078: 6c 67 37 33 dd 77 71 e3 - 0080: f5 31 03 9a 7e 4e a5 0e - 0088: 8b c8 9d af 89 b1 de 99 - 0090: 5c 20 36 98 cb 1a 69 a8 - 0098: db 5d 71 8c eb 7e 5e 0f - 00a0: c6 c9 63 52 e0 96 e2 57 - 00a8: 70 a4 d3 cc f3 e8 90 8a - 00b0: 46 03 c1 71 e3 74 c6 6b - 00b8: 73 98 82 e4 b7 59 5b 64 - 00c0: 23 8d 38 1d 33 45 82 d9 - 00c8: b6 11 7a f1 c7 05 c7 37 - 00d0: 75 c8 ee d5 4b 76 69 07 - 00d8: 2c db 4b 6f b9 29 c2 85 - 00e0: 22 57 cc b4 1d e1 f0 b3 - 00e8: 00 7e 96 ae fe de e3 49 - 00f0: 4a 95 98 f7 29 c6 9e 8e - 00f8: 52 10 c2 30 27 85 27 05 - 0100: 87 61 83 c5 c2 15 aa 3a - 0108: d5 0d 94 df 25 e8 64 66 - 0110: 0d b6 26 4f 14 ac d2 84 - 0118: 34 2d 31 ad 8a d4 f0 bc - 0120: 93 eb 20 b8 7e a6 ee 41 - 0128: 6b a0 fd a4 71 6c 36 86 - 0130: df 83 fa 21 b6 b7 8c 4d - 0138: e8 7a 27 09 f2 95 37 7d - 0140: e3 7b 4a 9d 74 f3 7f 4c - 0148: 12 b7 c0 2a 18 c6 ea f0 - 0150: ca 39 3a c6 bf c5 d3 2d - 0158: 46 a0 83 ab 61 a4 fa e2 - 0160: 8a ae db 0c c9 73 d1 df - 0168: 8a 43 30 32 6f cf fa 30 - 0170: 02 ae d2 6a 50 5d e8 0d - 0178: a1 33 e5 38 8e bd 8a fa - 0180: 5b 35 a9 6f f9 af 19 32 - 0188: 2a ba 6e e9 63 b4 05 71 - 0190: 81 8d 7b 1b 1f 36 e4 60 +listing: 15 child(ren) + child 0x1a: f1f3aa8a784d1361fc809d57df14a1955f8956c55e120e19 + child 0x1e: b13b7c8a2f30ffe9911f9507721dd41f2718d2d6d0fd1da0 + child 0x36: a93c14a59c62f37321d21557b0e821de471a7e3d079c6fce + child 0x47: 6bbe84feb97e48e1ae522ccd13793b6574c2777efb6dee5c + child 0x5b: 9c4eb9d5fe911b7d5e4ed9f0c5cf1bc192e715c7efec964e + child 0x5d: 13d7f7d066b80413cbbf47191b8f0279bc51ed7b0ce2aacc + child 0x5e: 1858235ee7f18e370085cc932b286588358a914bbada3414 + child 0x74: d97d914facb39def2423ff7905152aae5f43205fc66e09b7 + child 0x83: 55c53f50187c26e8c66b15499ff80b250e2e4137d7fd0509 + child 0x9b: 128101b9c92b93bfb6ab5ae172b728f8dd550ac4e8d154eb + child 0xc0: ff0b7fe81fb5ee50e91030be1e97d38df4d74655739d1d9c + child 0xc6: 23d3a3698c06decd9cc6888d1d692ad5976827ae3cc38e81 + child 0xe9: a02232f72cb56d04bb218f4a9b8a698f95f5cfd6b4e6f227 + child 0xf2: 42bb8c7ce00cc0681c05edde50af1bd47d5d94d0a136c882 + child 0xf8: 4ef63107bdc439cfb254ecc2cc1cee9bf68aec015a8fee8f +listing frame: 379 bytes + 0000: 00 00 01 77 1a f1 f3 aa + 0008: 8a 78 4d 13 61 fc 80 9d + 0010: 57 df 14 a1 95 5f 89 56 + 0018: c5 5e 12 0e 19 1e b1 3b + 0020: 7c 8a 2f 30 ff e9 91 1f + 0028: 95 07 72 1d d4 1f 27 18 + 0030: d2 d6 d0 fd 1d a0 36 a9 + 0038: 3c 14 a5 9c 62 f3 73 21 + 0040: d2 15 57 b0 e8 21 de 47 + 0048: 1a 7e 3d 07 9c 6f ce 47 + 0050: 6b be 84 fe b9 7e 48 e1 + 0058: ae 52 2c cd 13 79 3b 65 + 0060: 74 c2 77 7e fb 6d ee 5c + 0068: 5b 9c 4e b9 d5 fe 91 1b + 0070: 7d 5e 4e d9 f0 c5 cf 1b + 0078: c1 92 e7 15 c7 ef ec 96 + 0080: 4e 5d 13 d7 f7 d0 66 b8 + 0088: 04 13 cb bf 47 19 1b 8f + 0090: 02 79 bc 51 ed 7b 0c e2 + 0098: aa cc 5e 18 58 23 5e e7 + 00a0: f1 8e 37 00 85 cc 93 2b + 00a8: 28 65 88 35 8a 91 4b ba + 00b0: da 34 14 74 d9 7d 91 4f + 00b8: ac b3 9d ef 24 23 ff 79 + 00c0: 05 15 2a ae 5f 43 20 5f + 00c8: c6 6e 09 b7 83 55 c5 3f + 00d0: 50 18 7c 26 e8 c6 6b 15 + 00d8: 49 9f f8 0b 25 0e 2e 41 + 00e0: 37 d7 fd 05 09 9b 12 81 + 00e8: 01 b9 c9 2b 93 bf b6 ab + 00f0: 5a e1 72 b7 28 f8 dd 55 + 00f8: 0a c4 e8 d1 54 eb c0 ff + 0100: 0b 7f e8 1f b5 ee 50 e9 + 0108: 10 30 be 1e 97 d3 8d f4 + 0110: d7 46 55 73 9d 1d 9c c6 + 0118: 23 d3 a3 69 8c 06 de cd + 0120: 9c c6 88 8d 1d 69 2a d5 + 0128: 97 68 27 ae 3c c3 8e 81 + 0130: e9 a0 22 32 f7 2c b5 6d + 0138: 04 bb 21 8f 4a 9b 8a 69 + 0140: 8f 95 f5 cf d6 b4 e6 f2 + 0148: 27 f2 42 bb 8c 7c e0 0c + 0150: c0 68 1c 05 ed de 50 af + 0158: 1b d4 7d 5d 94 d0 a1 36 + 0160: c8 82 f8 4e f6 31 07 bd + 0168: c4 39 cf b2 54 ec c2 cc + 0170: 1c ee 9b f6 8a ec 01 5a + 0178: 8f ee 8f Initiator stream 0 (height 31), epoch 0 frame 0: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 9), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 70 9c 18 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 3), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 73 c0 12 frame 1: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 5), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 71 70 14 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 10), message 2 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 42 70 ac 18 19 frame 2: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 13), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 70 dc 1c 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 2), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 72 c0 11 frame 3: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 10), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 70 ac 19 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 12), message 2 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 42 70 cc 18 1b frame 4: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 6), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 71 b0 15 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 15), message 2 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 42 70 fc 18 1e frame 5: Supply(Continue) - supply run: 1 record(s) - record 0: version (0, 0, 1), message 8 byte(s) - 0000: 66 00 00 00 0d 00 00 00 - 0008: 09 77 10 00 00 00 00 00 - 0010: 00 00 + supply run: 2 record(s) + record 0: version (0, 0, 7), message 1 byte(s) + record 1: version (0, 0, 4), message 1 byte(s) + 0000: 66 00 00 00 10 00 00 00 + 0008: 04 42 71 f0 16 00 00 00 + 0010: 04 42 71 30 13 frame 6: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 11), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 70 bc 1a 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 8), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 70 8c 17 frame 7: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 7), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 71 f0 16 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 9), message 2 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 42 70 9c 18 18 frame 8: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 14), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 70 ec 1d 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 6), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 71 b0 15 frame 9: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 2), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 72 c0 11 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 5), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 71 70 14 frame 10: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 4), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 71 30 13 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 13), message 2 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 42 70 dc 18 1c frame 11: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 3), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 73 c0 12 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 11), message 2 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 42 70 bc 18 1a frame 12: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 12), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 70 cc 1b 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 14), message 2 byte(s) + 0000: 66 00 00 00 09 00 00 00 + 0008: 05 42 70 ec 18 1d frame 13: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 8), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 70 8c 17 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 1), message 1 byte(s) + 0000: 66 00 00 00 07 00 00 00 + 0008: 03 41 77 10 frame 14: Supply(End) supply run: 1 record(s) - record 0: version (0, 0, 15), message 8 byte(s) - 0000: 77 00 00 00 0e 00 00 00 - 0008: 0a 70 fc 1e 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 16), message 2 byte(s) + 0000: 77 00 00 00 09 00 00 00 + 0008: 05 42 70 43 18 1f frame 15: End(Stream) 0000: 99 Initiator stream 1 (height 30), epoch 0 @@ -416,14 +373,10 @@ Initiator stream 1 (height 30), epoch 0 0000: 89 frame 6: End(Reply) 0000: 89 - frame 7: QueryEmpty(Continue) - 0000: 23 - frame 8: Supply(End) - supply run: 1 record(s) - record 0: version (0, 0, 16), message 8 byte(s) - 0000: 78 00 00 00 0e 00 00 00 - 0008: 0a 70 43 1f 00 00 00 00 - 0010: 00 00 00 + frame 7: End(Reply) + 0000: 89 + frame 8: End(Reply) + 0000: 89 frame 9: End(Reply) 0000: 89 frame 10: End(Reply) @@ -436,11 +389,7 @@ Initiator stream 1 (height 30), epoch 0 0000: 89 frame 14: End(Reply) 0000: 89 - frame 15: End(Reply) - 0000: 89 - frame 16: End(Reply) - 0000: 89 - frame 17: End(Stream) + frame 15: End(Stream) 0000: 9a trailing frame: 1 bytes 0000: 2e diff --git a/tests/snapshots/gossip_snapshot__early_supplies_honor_redactions.snap b/tests/snapshots/gossip_snapshot__early_supplies_honor_redactions.snap index eca4fe30d..24f515dd8 100644 --- a/tests/snapshots/gossip_snapshot__early_supplies_honor_redactions.snap +++ b/tests/snapshots/gossip_snapshot__early_supplies_honor_redactions.snap @@ -8,28 +8,28 @@ preamble: 25 bytes 0008: 8c 18 96 68 c9 e4 83 72 0010: 37 bf 31 e0 2d 7f 6b 70 0018: 00 -version: (1, 1, 0) -greeting words: set len 2, version-size bound 2, message-size target 1638912 -version frame: 30 bytes - 0000: 00 00 00 1a 02 00 00 00 - 0008: 00 00 00 00 02 00 00 00 +version: (1, 4095, 0) +greeting words: set len 2, version-size bound 5, message-size target 1638912 +version frame: 35 bytes + 0000: 00 00 00 1f 02 00 00 00 + 0008: 00 00 00 00 05 00 00 00 0010: 00 00 00 00 00 02 19 00 - 0018: 00 00 00 00 5d 40 + 0018: 00 00 00 00 40 02 00 30 + 0020: 00 ff f4 listing: 1 child(ren) - child 0x9: 3629eebb207d33da9f5bbe95276a1a4c70a7e07aaa96d551 -listing frame: 33 bytes - 0000: 00 00 00 1d 01 00 00 00 - 0008: 09 36 29 ee bb 20 7d 33 - 0010: da 9f 5b be 95 27 6a 1a - 0018: 4c 70 a7 e0 7a aa 96 d5 - 0020: 51 + child 0x9a: 636d485e0ec23826ab88a639e80fe993d036c716de1441a1 +listing frame: 29 bytes + 0000: 00 00 00 19 9a 63 6d 48 + 0008: 5e 0e c2 38 26 ab 88 a6 + 0010: 39 e8 0f e9 93 d0 36 c7 + 0018: 16 de 14 41 a1 Initiator stream 0 (height 31), epoch 0 frame 0: Supply(End) supply run: 1 record(s) - record 0: version (1, 1, 0), message 8 byte(s) - 0000: 77 00 00 00 0e 00 00 00 - 0008: 0a 5d 40 a5 00 00 00 00 - 0010: 00 00 00 + record 0: version (1, 432, 0), message 3 byte(s) + 0000: 77 00 00 00 0d 00 00 00 + 0008: 09 45 40 36 50 06 c1 19 + 0010: 01 b1 frame 1: End(Stream) 0000: 99 Initiator stream 1 (height 30), epoch 0 @@ -46,50 +46,46 @@ preamble: 25 bytes 0008: 8c 18 96 68 c9 e4 83 72 0010: 37 bf 31 e0 2d 7f 6b 70 0018: 00 -version: (1, 0, 4) +version: (1, 0, 30) greeting words: set len 3, version-size bound 2, message-size target 1638912 -version frame: 30 bytes - 0000: 00 00 00 1a 03 00 00 00 +version frame: 31 bytes + 0000: 00 00 00 1b 03 00 00 00 0008: 00 00 00 00 02 00 00 00 0010: 00 00 00 00 00 02 19 00 - 0018: 00 00 00 00 54 4c + 0018: 00 00 00 00 54 1e c0 listing: 3 child(ren) - child 0x94: 9ba2ba4f4d18c5228db4534e273695fc766b743561127128 - child 0xcf: b917fe6f40e679e54da764fcbef729b0277f9e3afd6dc562 - child 0xe9: 4747eda671d8a6acf6c0c83a7f1c1ad1255b1429364fbc36 -listing frame: 83 bytes - 0000: 00 00 00 4f 03 00 00 00 - 0008: 94 9b a2 ba 4f 4d 18 c5 - 0010: 22 8d b4 53 4e 27 36 95 - 0018: fc 76 6b 74 35 61 12 71 - 0020: 28 cf b9 17 fe 6f 40 e6 - 0028: 79 e5 4d a7 64 fc be f7 - 0030: 29 b0 27 7f 9e 3a fd 6d - 0038: c5 62 e9 47 47 ed a6 71 - 0040: d8 a6 ac f6 c0 c8 3a 7f - 0048: 1c 1a d1 25 5b 14 29 36 - 0050: 4f bc 36 + child 0x90: 937506250024599968f64f7c4e2f3ac050a148a65441140d + child 0xab: 0e0630bedb15ab6e5a395048d02bb03e950f377b95eed0b1 + child 0xfa: 096df9d6e522813481d0f18c5e7cb824eb404882002c26a2 +listing frame: 79 bytes + 0000: 00 00 00 4b 90 93 75 06 + 0008: 25 00 24 59 99 68 f6 4f + 0010: 7c 4e 2f 3a c0 50 a1 48 + 0018: a6 54 41 14 0d ab 0e 06 + 0020: 30 be db 15 ab 6e 5a 39 + 0028: 50 48 d0 2b b0 3e 95 0f + 0030: 37 7b 95 ee d0 b1 fa 09 + 0038: 6d f9 d6 e5 22 81 34 81 + 0040: d0 f1 8c 5e 7c b8 24 eb + 0048: 40 48 82 00 2c 26 a2 Responder stream 0 (height 31), epoch 0 - frame 0: QueryEmpty(Continue) - 0000: 22 - frame 1: Supply(Continue) + frame 0: Supply(Continue) supply run: 1 record(s) - record 0: version (1, 0, 3), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 54 f0 65 00 00 00 00 - 0010: 00 00 00 + record 0: version (1, 0, 4), message 3 byte(s) + 0000: 66 00 00 00 0a 00 00 00 + 0008: 06 42 54 4c 19 27 12 + frame 1: QueryEmpty(Continue) + 0000: 22 frame 2: Supply(Continue) supply run: 1 record(s) - record 0: version (1, 0, 4), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 54 4c 66 00 00 00 00 - 0010: 00 00 00 + record 0: version (1, 0, 2), message 3 byte(s) + 0000: 66 00 00 00 0a 00 00 00 + 0008: 06 42 54 b0 19 27 10 frame 3: Supply(End) supply run: 1 record(s) - record 0: version (1, 0, 2), message 8 byte(s) - 0000: 77 00 00 00 0e 00 00 00 - 0008: 0a 54 b0 64 00 00 00 00 - 0010: 00 00 00 + record 0: version (1, 0, 3), message 3 byte(s) + 0000: 77 00 00 00 0a 00 00 00 + 0008: 06 42 54 f0 19 27 11 frame 4: End(Stream) 0000: 99 trailing frame: 1 bytes diff --git a/tests/snapshots/gossip_snapshot__empty_pair_converges_immediately.snap b/tests/snapshots/gossip_snapshot__empty_pair_converges_immediately.snap index afc6d5e63..45fff58ab 100644 --- a/tests/snapshots/gossip_snapshot__empty_pair_converges_immediately.snap +++ b/tests/snapshots/gossip_snapshot__empty_pair_converges_immediately.snap @@ -16,8 +16,8 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 1 bytes 0000: 2e @@ -35,7 +35,7 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 1 bytes 0000: 2e diff --git a/tests/snapshots/gossip_snapshot__fork_insert_redact.snap b/tests/snapshots/gossip_snapshot__fork_insert_redact.snap index 17ef974bf..e6311ca7c 100644 --- a/tests/snapshots/gossip_snapshot__fork_insert_redact.snap +++ b/tests/snapshots/gossip_snapshot__fork_insert_redact.snap @@ -16,28 +16,26 @@ version frame: 30 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 4b 24 listing: 2 child(ren) - child 0x9c: 5b97e7f3a07810e9137936dfdc8fa30e76e419b6b3b8a0e7 - child 0xa0: b7e99b381ba2cb4d2f4fef624ed06674d9e0a367d05272d6 -listing frame: 58 bytes - 0000: 00 00 00 36 02 00 00 00 - 0008: 9c 5b 97 e7 f3 a0 78 10 - 0010: e9 13 79 36 df dc 8f a3 - 0018: 0e 76 e4 19 b6 b3 b8 a0 - 0020: e7 a0 b7 e9 9b 38 1b a2 - 0028: cb 4d 2f 4f ef 62 4e d0 - 0030: 66 74 d9 e0 a3 67 d0 52 - 0038: 72 d6 + child 0x64: bb28d1c9e9fa40672e21164802bc8e7a23eb03126468e82c + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa +listing frame: 54 bytes + 0000: 00 00 00 32 64 bb 28 d1 + 0008: c9 e9 fa 40 67 2e 21 16 + 0010: 48 02 bc 8e 7a 23 eb 03 + 0018: 12 64 68 e8 2c 7c f5 3f + 0020: 94 77 30 97 88 c6 ed 88 + 0028: 34 00 8f 39 4a 3b e5 f2 + 0030: 12 95 9f ac 55 fa Responder stream 0 (height 31), epoch 0 - frame 0: QueryEmpty(Continue) - 0000: 22 + frame 0: Supply(Continue) + supply run: 1 record(s) + record 0: version (2, 1, 0), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 49 50 03 frame 1: QueryEmpty(Continue) 0000: 22 - frame 2: Supply(End) - supply run: 1 record(s) - record 0: version (2, 1, 0), message 8 byte(s) - 0000: 77 00 00 00 0e 00 00 00 - 0008: 0a 49 50 03 00 00 00 00 - 0010: 00 00 00 + frame 2: QueryEmpty(End) + 0000: 33 frame 3: End(Stream) 0000: 99 trailing frame: 1 bytes @@ -57,24 +55,22 @@ version frame: 30 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 5c b0 listing: 2 child(ren) - child 0x9: 7ca8b07cb28254109eb3f89675cdb22ab6882b6ab31b3cdf - child 0x57: 8c3f0107d1645e45e31c5c5bd2c2c84f457fdce162fe19f4 -listing frame: 58 bytes - 0000: 00 00 00 36 02 00 00 00 - 0008: 09 7c a8 b0 7c b2 82 54 - 0010: 10 9e b3 f8 96 75 cd b2 - 0018: 2a b6 88 2b 6a b3 1b 3c - 0020: df 57 8c 3f 01 07 d1 64 - 0028: 5e 45 e3 1c 5c 5b d2 c2 - 0030: c8 4f 45 7f dc e1 62 fe - 0038: 19 f4 + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 + child 0xe7: c9b78024d3d0ad1a5523b4ac969d289fee0651305c416d72 +listing frame: 54 bytes + 0000: 00 00 00 32 9a cc a7 fd + 0008: 1f e9 57 c5 26 7c e1 59 + 0010: c8 f3 fe 95 bc 4f 49 6f + 0018: 6b 2f b5 fa 39 e7 c9 b7 + 0020: 80 24 d3 d0 ad 1a 55 23 + 0028: b4 ac 96 9d 28 9f ee 06 + 0030: 51 30 5c 41 6d 72 Initiator stream 0 (height 31), epoch 0 frame 0: Supply(End) supply run: 1 record(s) - record 0: version (2, 0, 1), message 8 byte(s) - 0000: 77 00 00 00 0e 00 00 00 - 0008: 0a 5d c0 04 00 00 00 00 - 0010: 00 00 00 + record 0: version (2, 0, 1), message 1 byte(s) + 0000: 77 00 00 00 08 00 00 00 + 0008: 04 42 5d c0 04 frame 1: End(Stream) 0000: 99 Initiator stream 1 (height 30), epoch 0 diff --git a/tests/snapshots/gossip_snapshot__one_sided_transfer.snap b/tests/snapshots/gossip_snapshot__one_sided_transfer.snap index f64e5c2ea..b2c6a0d0c 100644 --- a/tests/snapshots/gossip_snapshot__one_sided_transfer.snap +++ b/tests/snapshots/gossip_snapshot__one_sided_transfer.snap @@ -16,30 +16,27 @@ version frame: 30 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 5c 90 listing: 2 child(ren) - child 0x71: b0e862e82b7b87723797a3653b59d1f8d0f3146ccef3a062 - child 0x7e: 84d4b266ab26a5ecfc7c2e8ea2154ebe187883a8aba00b3e -listing frame: 58 bytes - 0000: 00 00 00 36 02 00 00 00 - 0008: 71 b0 e8 62 e8 2b 7b 87 - 0010: 72 37 97 a3 65 3b 59 d1 - 0018: f8 d0 f3 14 6c ce f3 a0 - 0020: 62 7e 84 d4 b2 66 ab 26 - 0028: a5 ec fc 7c 2e 8e a2 15 - 0030: 4e be 18 78 83 a8 ab a0 - 0038: 0b 3e + child 0x93: c4c0139f3f4f4afe9715d26a43953691cdc861574ffc8196 + child 0xa0: 7835b0a8791af76763e8248004d356e7d5c938bb893ecc9c +listing frame: 54 bytes + 0000: 00 00 00 32 93 c4 c0 13 + 0008: 9f 3f 4f 4a fe 97 15 d2 + 0010: 6a 43 95 36 91 cd c8 61 + 0018: 57 4f fc 81 96 a0 78 35 + 0020: b0 a8 79 1a f7 67 63 e8 + 0028: 24 80 04 d3 56 e7 d5 c9 + 0030: 38 bb 89 3e cc 9c Responder stream 0 (height 31), epoch 0 frame 0: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 1, 0), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 55 40 01 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 1, 0), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 55 40 01 frame 1: Supply(End) supply run: 1 record(s) - record 0: version (0, 2, 0), message 8 byte(s) - 0000: 77 00 00 00 0e 00 00 00 - 0008: 0a 5c 90 02 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 2, 0), message 1 byte(s) + 0000: 77 00 00 00 08 00 00 00 + 0008: 04 42 5c 90 02 frame 2: End(Stream) 0000: 99 trailing frame: 1 bytes @@ -59,7 +56,7 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 1 bytes 0000: 2e diff --git a/tests/snapshots/gossip_snapshot__redaction_only.snap b/tests/snapshots/gossip_snapshot__redaction_only.snap index 47a3874d5..a3c62a93e 100644 --- a/tests/snapshots/gossip_snapshot__redaction_only.snap +++ b/tests/snapshots/gossip_snapshot__redaction_only.snap @@ -16,13 +16,12 @@ version frame: 30 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 49 50 listing: 1 child(ren) - child 0xa0: b7e99b381ba2cb4d2f4fef624ed06674d9e0a367d05272d6 -listing frame: 33 bytes - 0000: 00 00 00 1d 01 00 00 00 - 0008: a0 b7 e9 9b 38 1b a2 cb - 0010: 4d 2f 4f ef 62 4e d0 66 - 0018: 74 d9 e0 a3 67 d0 52 72 - 0020: d6 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa +listing frame: 29 bytes + 0000: 00 00 00 19 7c f5 3f 94 + 0008: 77 30 97 88 c6 ed 88 34 + 0010: 00 8f 39 4a 3b e5 f2 12 + 0018: 95 9f ac 55 fa trailing frame: 1 bytes 0000: 2e @@ -40,17 +39,16 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 b8 listing: 2 child(ren) - child 0x9: 7ca8b07cb28254109eb3f89675cdb22ab6882b6ab31b3cdf - child 0xa0: b7e99b381ba2cb4d2f4fef624ed06674d9e0a367d05272d6 -listing frame: 58 bytes - 0000: 00 00 00 36 02 00 00 00 - 0008: 09 7c a8 b0 7c b2 82 54 - 0010: 10 9e b3 f8 96 75 cd b2 - 0018: 2a b6 88 2b 6a b3 1b 3c - 0020: df a0 b7 e9 9b 38 1b a2 - 0028: cb 4d 2f 4f ef 62 4e d0 - 0030: 66 74 d9 e0 a3 67 d0 52 - 0038: 72 d6 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 +listing frame: 54 bytes + 0000: 00 00 00 32 7c f5 3f 94 + 0008: 77 30 97 88 c6 ed 88 34 + 0010: 00 8f 39 4a 3b e5 f2 12 + 0018: 95 9f ac 55 fa 9a cc a7 + 0020: fd 1f e9 57 c5 26 7c e1 + 0028: 59 c8 f3 fe 95 bc 4f 49 + 0030: 6f 6b 2f b5 fa 39 Responder stream 0 (height 31), epoch 0 frame 0: Match(End) 0000: 11 diff --git a/tests/snapshots/gossip_snapshot__same_live_content_divergent_versions.snap b/tests/snapshots/gossip_snapshot__same_live_content_divergent_versions.snap index 4d999db27..245a6a355 100644 --- a/tests/snapshots/gossip_snapshot__same_live_content_divergent_versions.snap +++ b/tests/snapshots/gossip_snapshot__same_live_content_divergent_versions.snap @@ -16,13 +16,12 @@ version frame: 30 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 49 24 listing: 1 child(ren) - child 0x9: 7ca8b07cb28254109eb3f89675cdb22ab6882b6ab31b3cdf -listing frame: 33 bytes - 0000: 00 00 00 1d 01 00 00 00 - 0008: 09 7c a8 b0 7c b2 82 54 - 0010: 10 9e b3 f8 96 75 cd b2 - 0018: 2a b6 88 2b 6a b3 1b 3c - 0020: df + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 +listing frame: 29 bytes + 0000: 00 00 00 19 9a cc a7 fd + 0008: 1f e9 57 c5 26 7c e1 59 + 0010: c8 f3 fe 95 bc 4f 49 6f + 0018: 6b 2f b5 fa 39 Responder stream 0 (height 31), epoch 0 frame 0: Match(End) 0000: 11 @@ -45,12 +44,11 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 a8 listing: 1 child(ren) - child 0x9: 7ca8b07cb28254109eb3f89675cdb22ab6882b6ab31b3cdf -listing frame: 33 bytes - 0000: 00 00 00 1d 01 00 00 00 - 0008: 09 7c a8 b0 7c b2 82 54 - 0010: 10 9e b3 f8 96 75 cd b2 - 0018: 2a b6 88 2b 6a b3 1b 3c - 0020: df + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 +listing frame: 29 bytes + 0000: 00 00 00 19 9a cc a7 fd + 0008: 1f e9 57 c5 26 7c e1 59 + 0010: c8 f3 fe 95 bc 4f 49 6f + 0018: 6b 2f b5 fa 39 trailing frame: 1 bytes 0000: 2e diff --git a/tests/snapshots/gossip_snapshot__string_payload.snap b/tests/snapshots/gossip_snapshot__string_payload.snap index 8a4c43e54..6c31b259a 100644 --- a/tests/snapshots/gossip_snapshot__string_payload.snap +++ b/tests/snapshots/gossip_snapshot__string_payload.snap @@ -16,22 +16,21 @@ version frame: 30 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 55 40 listing: 1 child(ren) - child 0xed: 718f933fd8c12515f7edbe713525538ab327518e6e869d4c -listing frame: 33 bytes - 0000: 00 00 00 1d 01 00 00 00 - 0008: ed 71 8f 93 3f d8 c1 25 - 0010: 15 f7 ed be 71 35 25 53 - 0018: 8a b3 27 51 8e 6e 86 9d - 0020: 4c + child 0x93: c4c0139f3f4f4afe9715d26a43953691cdc861574ffc8196 +listing frame: 29 bytes + 0000: 00 00 00 19 93 c4 c0 13 + 0008: 9f 3f 4f 4a fe 97 15 d2 + 0010: 6a 43 95 36 91 cd c8 61 + 0018: 57 4f fc 81 96 Responder stream 0 (height 31), epoch 0 - frame 0: QueryEmpty(Continue) - 0000: 22 - frame 1: Supply(End) + frame 0: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 1, 0), message 9 byte(s) - 0000: 77 00 00 00 0f 00 00 00 - 0008: 0b 55 40 05 00 00 00 68 - 0010: 65 6c 6c 6f + record 0: version (0, 1, 0), message 6 byte(s) + 0000: 66 00 00 00 0d 00 00 00 + 0008: 09 42 55 40 65 68 65 6c + 0010: 6c 6f + frame 1: QueryEmpty(End) + 0000: 33 frame 2: End(Stream) 0000: 99 trailing frame: 1 bytes @@ -51,20 +50,19 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 77 listing: 1 child(ren) - child 0xa6: 0f640284bd5ca5d334f996c8bed24b4b70dd3d1e42805409 -listing frame: 33 bytes - 0000: 00 00 00 1d 01 00 00 00 - 0008: a6 0f 64 02 84 bd 5c a5 - 0010: d3 34 f9 96 c8 be d2 4b - 0018: 4b 70 dd 3d 1e 42 80 54 - 0020: 09 + child 0xf2: 42bb8c7ce00cc0681c05edde50af1bd47d5d94d0a136c882 +listing frame: 29 bytes + 0000: 00 00 00 19 f2 42 bb 8c + 0008: 7c e0 0c c0 68 1c 05 ed + 0010: de 50 af 1b d4 7d 5d 94 + 0018: d0 a1 36 c8 82 Initiator stream 0 (height 31), epoch 0 frame 0: Supply(End) supply run: 1 record(s) - record 0: version (0, 0, 1), message 9 byte(s) - 0000: 77 00 00 00 0e 00 00 00 - 0008: 0a 77 05 00 00 00 77 6f - 0010: 72 6c 64 + record 0: version (0, 0, 1), message 6 byte(s) + 0000: 77 00 00 00 0c 00 00 00 + 0008: 08 41 77 65 77 6f 72 6c + 0010: 64 frame 1: End(Stream) 0000: 99 Initiator stream 1 (height 30), epoch 0 diff --git a/tests/snapshots/gossip_snapshot__v1_one_sided_transfer.snap b/tests/snapshots/gossip_snapshot__v1_one_sided_transfer.snap index 6ca00786e..e639ef9ad 100644 --- a/tests/snapshots/gossip_snapshot__v1_one_sided_transfer.snap +++ b/tests/snapshots/gossip_snapshot__v1_one_sided_transfer.snap @@ -14,36 +14,35 @@ received 25 bytes │ received 25 bytes 0008: 8c 18 96 68 c9 e4 83 72 │ 0008: 8c 18 96 68 c9 e4 83 72 0010: 37 bf 31 e0 2d 7f 6b 70 │ 0010: 37 bf 31 e0 2d 7f 6b 70 0018: 00 │ 0018: 00 -sent 6 bytes │ sent 5 bytes - 0000: 00 00 00 02 5c 90 │ 0000: 00 00 00 01 e0 -received 13 bytes │ received 6 bytes - 0000: 00 00 00 01 e0 00 00 00 │ 0000: 00 00 00 02 5c 90 - 0008: 04 00 00 00 00 │ sent 8 bytes +sent 7 bytes │ sent 6 bytes + 0000: 00 00 00 03 42 5c 90 │ 0000: 00 00 00 02 41 e0 +received 14 bytes │ received 7 bytes + 0000: 00 00 00 02 41 e0 00 00 │ 0000: 00 00 00 03 42 5c 90 + 0008: 00 04 00 00 00 00 │ sent 8 bytes sent 58 bytes │ 0000: 00 00 00 04 00 00 00 00 0000: 00 00 00 36 02 00 00 00 │ received 58 bytes - 0008: 71 b0 e8 62 e8 2b 7b 87 │ 0000: 00 00 00 36 02 00 00 00 - 0010: 72 37 97 a3 65 3b 59 d1 │ 0008: 71 b0 e8 62 e8 2b 7b 87 - 0018: f8 d0 f3 14 6c ce f3 a0 │ 0010: 72 37 97 a3 65 3b 59 d1 - 0020: 62 7e 84 d4 b2 66 ab 26 │ 0018: f8 d0 f3 14 6c ce f3 a0 - 0028: a5 ec fc 7c 2e 8e a2 15 │ 0020: 62 7e 84 d4 b2 66 ab 26 - 0030: 4e be 18 78 83 a8 ab a0 │ 0028: a5 ec fc 7c 2e 8e a2 15 - 0038: 0b 3e │ 0030: 4e be 18 78 83 a8 ab a0 -received 18 bytes │ 0038: 0b 3e + 0008: 93 c4 c0 13 9f 3f 4f 4a │ 0000: 00 00 00 36 02 00 00 00 + 0010: fe 97 15 d2 6a 43 95 36 │ 0008: 93 c4 c0 13 9f 3f 4f 4a + 0018: 91 cd c8 61 57 4f fc 81 │ 0010: fe 97 15 d2 6a 43 95 36 + 0020: 96 a0 78 35 b0 a8 79 1a │ 0018: 91 cd c8 61 57 4f fc 81 + 0028: f7 67 63 e8 24 80 04 d3 │ 0020: 96 a0 78 35 b0 a8 79 1a + 0030: 56 e7 d5 c9 38 bb 89 3e │ 0028: f7 67 63 e8 24 80 04 d3 + 0038: cc 9c │ 0030: 56 e7 d5 c9 38 bb 89 3e +received 18 bytes │ 0038: cc 9c 0000: 00 00 00 0e 00 00 00 00 │ sent 18 bytes - 0008: 02 00 00 00 71 7e 00 00 │ 0000: 00 00 00 0e 00 00 00 00 - 0010: 00 00 │ 0008: 02 00 00 00 71 7e 00 00 -sent 102 bytes │ 0010: 00 00 - 0000: 00 00 00 62 02 00 00 00 │ received 102 bytes - 0008: 71 06 1e 47 fd 7f c6 23 │ 0000: 00 00 00 62 02 00 00 00 - 0010: ab c7 8d 36 e9 8c da ab │ 0008: 71 06 1e 47 fd 7f c6 23 - 0018: 80 b4 4e 92 39 b6 59 bf │ 0010: ab c7 8d 36 e9 8c da ab - 0020: 25 be 0f 2e b7 ec e6 da │ 0018: 80 b4 4e 92 39 b6 59 bf - 0028: 98 55 40 01 00 00 00 00 │ 0020: 25 be 0f 2e b7 ec e6 da - 0030: 00 00 00 7e f9 1e 52 73 │ 0028: 98 55 40 01 00 00 00 00 - 0038: 39 26 57 44 a6 3e cf 3d │ 0030: 00 00 00 7e f9 1e 52 73 - 0040: 77 55 ae a6 a8 d6 6e 45 │ 0038: 39 26 57 44 a6 3e cf 3d - 0048: 3e 17 c5 ad af b5 8d 13 │ 0040: 77 55 ae a6 a8 d6 6e 45 - 0050: e0 a8 a2 2c 5c 90 02 00 │ 0048: 3e 17 c5 ad af b5 8d 13 - 0058: 00 00 00 00 00 00 00 00 │ 0050: e0 a8 a2 2c 5c 90 02 00 - 0060: 00 00 00 00 00 00 │ 0058: 00 00 00 00 00 00 00 00 - │ 0060: 00 00 00 00 00 00 + 0008: 02 00 00 00 93 a0 00 00 │ 0000: 00 00 00 0e 00 00 00 00 + 0010: 00 00 │ 0008: 02 00 00 00 93 a0 00 00 +sent 92 bytes │ 0010: 00 00 + 0000: 00 00 00 58 02 00 00 00 │ received 92 bytes + 0008: 93 40 1e 39 7b d9 a3 32 │ 0000: 00 00 00 58 02 00 00 00 + 0010: 70 67 e1 41 f1 93 fd 25 │ 0008: 93 40 1e 39 7b d9 a3 32 + 0018: 9d 73 bb 56 ce 1b 4d 5a │ 0010: 70 67 e1 41 f1 93 fd 25 + 0020: 6e 29 1a f9 b8 22 bd 08 │ 0018: 9d 73 bb 56 ce 1b 4d 5a + 0028: 80 42 55 40 41 01 a0 ea │ 0020: 6e 29 1a f9 b8 22 bd 08 + 0030: 1e 3c b9 10 d3 31 5f b0 │ 0028: 80 42 55 40 41 01 a0 ea + 0038: 54 3b 06 56 cb 47 e1 d1 │ 0030: 1e 3c b9 10 d3 31 5f b0 + 0040: 78 83 24 66 3a 24 ba ed │ 0038: 54 3b 06 56 cb 47 e1 d1 + 0048: 76 21 97 1c 14 ac 2b 42 │ 0040: 78 83 24 66 3a 24 ba ed + 0050: 5c 90 41 02 00 00 00 00 │ 0048: 76 21 97 1c 14 ac 2b 42 + 0058: 00 00 00 00 │ 0050: 5c 90 41 02 00 00 00 00 + │ 0058: 00 00 00 00 diff --git a/tests/snapshots/retire_snapshot__divergent_retire.snap b/tests/snapshots/retire_snapshot__divergent_retire.snap index 5c8987ca3..7d04a4304 100644 --- a/tests/snapshots/retire_snapshot__divergent_retire.snap +++ b/tests/snapshots/retire_snapshot__divergent_retire.snap @@ -16,22 +16,20 @@ version frame: 30 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 55 40 listing: 1 child(ren) - child 0xc0: 4e2095fab2a3d070533be5cc06f84713ff93bd2574cf2016 -listing frame: 33 bytes - 0000: 00 00 00 1d 01 00 00 00 - 0008: c0 4e 20 95 fa b2 a3 d0 - 0010: 70 53 3b e5 cc 06 f8 47 - 0018: 13 ff 93 bd 25 74 cf 20 - 0020: 16 + child 0x93: c4c0139f3f4f4afe9715d26a43953691cdc861574ffc8196 +listing frame: 29 bytes + 0000: 00 00 00 19 93 c4 c0 13 + 0008: 9f 3f 4f 4a fe 97 15 d2 + 0010: 6a 43 95 36 91 cd c8 61 + 0018: 57 4f fc 81 96 Responder stream 0 (height 31), epoch 0 - frame 0: QueryEmpty(Continue) - 0000: 22 - frame 1: Supply(End) + frame 0: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 1, 0), message 8 byte(s) - 0000: 77 00 00 00 0e 00 00 00 - 0008: 0a 55 40 02 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 1, 0), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 55 40 02 + frame 1: QueryEmpty(End) + 0000: 33 frame 2: End(Stream) 0000: 99 trailing frame: 1 bytes @@ -51,20 +49,18 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 77 listing: 1 child(ren) - child 0xd: c14a19589e521206ef56ae41b56bcf91761df869fc42fe2d -listing frame: 33 bytes - 0000: 00 00 00 1d 01 00 00 00 - 0008: 0d c1 4a 19 58 9e 52 12 - 0010: 06 ef 56 ae 41 b5 6b cf - 0018: 91 76 1d f8 69 fc 42 fe - 0020: 2d + child 0xf2: 42bb8c7ce00cc0681c05edde50af1bd47d5d94d0a136c882 +listing frame: 29 bytes + 0000: 00 00 00 19 f2 42 bb 8c + 0008: 7c e0 0c c0 68 1c 05 ed + 0010: de 50 af 1b d4 7d 5d 94 + 0018: d0 a1 36 c8 82 Initiator stream 0 (height 31), epoch 0 frame 0: Supply(End) supply run: 1 record(s) - record 0: version (0, 0, 1), message 8 byte(s) - 0000: 77 00 00 00 0d 00 00 00 - 0008: 09 77 01 00 00 00 00 00 - 0010: 00 00 + record 0: version (0, 0, 1), message 1 byte(s) + 0000: 77 00 00 00 07 00 00 00 + 0008: 03 41 77 01 frame 1: End(Stream) 0000: 99 Initiator stream 1 (height 30), epoch 0 diff --git a/tests/snapshots/retire_snapshot__empty_retire.snap b/tests/snapshots/retire_snapshot__empty_retire.snap index adaa7cb16..04ca1804f 100644 --- a/tests/snapshots/retire_snapshot__empty_retire.snap +++ b/tests/snapshots/retire_snapshot__empty_retire.snap @@ -16,8 +16,8 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 1 bytes 0000: 2e @@ -35,7 +35,7 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 6 bytes 0000: 00 00 00 01 48 2e diff --git a/tests/snapshots/retire_snapshot__retire_into_bootstrapper.snap b/tests/snapshots/retire_snapshot__retire_into_bootstrapper.snap index e1e9d78a0..0c1c7b55f 100644 --- a/tests/snapshots/retire_snapshot__retire_into_bootstrapper.snap +++ b/tests/snapshots/retire_snapshot__retire_into_bootstrapper.snap @@ -16,8 +16,8 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 e0 listing: 0 child(ren) -listing frame: 8 bytes - 0000: 00 00 00 04 00 00 00 00 +listing frame: 4 bytes + 0000: 00 00 00 00 trailing frame: 1 bytes 0000: 2e @@ -35,30 +35,27 @@ version frame: 30 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 72 c0 listing: 2 child(ren) - child 0xa: 793ebdb1644df56017a53f0e3fbf2ddcbc9a2845d9350368 - child 0xd: c14a19589e521206ef56ae41b56bcf91761df869fc42fe2d -listing frame: 58 bytes - 0000: 00 00 00 36 02 00 00 00 - 0008: 0a 79 3e bd b1 64 4d f5 - 0010: 60 17 a5 3f 0e 3f bf 2d - 0018: dc bc 9a 28 45 d9 35 03 - 0020: 68 0d c1 4a 19 58 9e 52 - 0028: 12 06 ef 56 ae 41 b5 6b - 0030: cf 91 76 1d f8 69 fc 42 - 0038: fe 2d + child 0x36: a93c14a59c62f37321d21557b0e821de471a7e3d079c6fce + child 0xf2: 42bb8c7ce00cc0681c05edde50af1bd47d5d94d0a136c882 +listing frame: 54 bytes + 0000: 00 00 00 32 36 a9 3c 14 + 0008: a5 9c 62 f3 73 21 d2 15 + 0010: 57 b0 e8 21 de 47 1a 7e + 0018: 3d 07 9c 6f ce f2 42 bb + 0020: 8c 7c e0 0c c0 68 1c 05 + 0028: ed de 50 af 1b d4 7d 5d + 0030: 94 d0 a1 36 c8 82 Responder stream 0 (height 31), epoch 0 frame 0: Supply(Continue) supply run: 1 record(s) - record 0: version (0, 0, 2), message 8 byte(s) - 0000: 66 00 00 00 0e 00 00 00 - 0008: 0a 72 c0 02 00 00 00 00 - 0010: 00 00 00 + record 0: version (0, 0, 2), message 1 byte(s) + 0000: 66 00 00 00 08 00 00 00 + 0008: 04 42 72 c0 02 frame 1: Supply(End) supply run: 1 record(s) - record 0: version (0, 0, 1), message 8 byte(s) - 0000: 77 00 00 00 0d 00 00 00 - 0008: 09 77 01 00 00 00 00 00 - 0010: 00 00 + record 0: version (0, 0, 1), message 1 byte(s) + 0000: 77 00 00 00 07 00 00 00 + 0008: 03 41 77 01 frame 2: End(Stream) 0000: 99 trailing frame: 6 bytes diff --git a/tests/snapshots/retire_snapshot__v1_divergent_retire.snap b/tests/snapshots/retire_snapshot__v1_divergent_retire.snap index 73cd722e3..e979846e4 100644 --- a/tests/snapshots/retire_snapshot__v1_divergent_retire.snap +++ b/tests/snapshots/retire_snapshot__v1_divergent_retire.snap @@ -14,38 +14,36 @@ received 25 bytes │ received 25 bytes 0008: 8c 18 96 68 c9 e4 83 72 │ 0008: 8c 18 96 68 c9 e4 83 72 0010: 37 bf 31 e0 2d 7f 6b 70 │ 0010: 37 bf 31 e0 2d 7f 6b 70 0018: 01 │ 0018: 00 -sent 6 bytes │ sent 5 bytes - 0000: 00 00 00 02 55 40 │ 0000: 00 00 00 01 77 -received 37 bytes │ received 6 bytes - 0000: 00 00 00 01 77 00 00 00 │ 0000: 00 00 00 02 55 40 - 0008: 1c 01 00 00 00 12 c6 9e │ sent 32 bytes - 0010: 8a 5b e3 ca 3a b2 1a b8 │ 0000: 00 00 00 1c 01 00 00 00 - 0018: c9 29 55 ff b2 5b 22 ef │ 0008: 12 c6 9e 8a 5b e3 ca 3a - 0020: 07 69 d1 38 53 │ 0010: b2 1a b8 c9 29 55 ff b2 -sent 33 bytes │ 0018: 5b 22 ef 07 69 d1 38 53 +sent 7 bytes │ sent 6 bytes + 0000: 00 00 00 03 42 55 40 │ 0000: 00 00 00 02 41 77 +received 38 bytes │ received 7 bytes + 0000: 00 00 00 02 41 77 00 00 │ 0000: 00 00 00 03 42 55 40 + 0008: 00 1c 01 00 00 00 78 e3 │ sent 32 bytes + 0010: 51 43 78 52 e4 1b a1 18 │ 0000: 00 00 00 1c 01 00 00 00 + 0018: b1 49 a0 6b 26 d9 2e 75 │ 0008: 78 e3 51 43 78 52 e4 1b + 0020: ab 1b 90 2d 41 c4 │ 0010: a1 18 b1 49 a0 6b 26 d9 +sent 33 bytes │ 0018: 2e 75 ab 1b 90 2d 41 c4 0000: 00 00 00 1d 01 00 00 00 │ received 33 bytes - 0008: c0 4e 20 95 fa b2 a3 d0 │ 0000: 00 00 00 1d 01 00 00 00 - 0010: 70 53 3b e5 cc 06 f8 47 │ 0008: c0 4e 20 95 fa b2 a3 d0 - 0018: 13 ff 93 bd 25 74 cf 20 │ 0010: 70 53 3b e5 cc 06 f8 47 - 0020: 16 │ 0018: 13 ff 93 bd 25 74 cf 20 -received 59 bytes │ 0020: 16 - 0000: 00 00 00 37 01 00 00 00 │ sent 59 bytes - 0008: 0d 1f 40 60 1d f6 30 c7 │ 0000: 00 00 00 37 01 00 00 00 - 0010: 79 83 dc e0 17 d4 1c b8 │ 0008: 0d 1f 40 60 1d f6 30 c7 - 0018: 3b ef 0b ab 39 51 4c 6a │ 0010: 79 83 dc e0 17 d4 1c b8 - 0020: 80 b0 e9 bd a2 43 29 5b │ 0018: 3b ef 0b ab 39 51 4c 6a - 0028: a5 77 01 00 00 00 00 00 │ 0020: 80 b0 e9 bd a2 43 29 5b - 0030: 00 00 01 00 00 00 c0 00 │ 0028: a5 77 01 00 00 00 00 00 - 0038: 00 00 00 │ 0030: 00 00 01 00 00 00 c0 00 -sent 59 bytes │ 0038: 00 00 00 - 0000: 00 00 00 37 01 00 00 00 │ received 59 bytes - 0008: c0 d8 1e 7d 50 5b f9 55 │ 0000: 00 00 00 37 01 00 00 00 - 0010: bb 12 69 5d 0b be ca 69 │ 0008: c0 d8 1e 7d 50 5b f9 55 - 0018: 3f f9 84 40 c5 d5 93 d5 │ 0010: bb 12 69 5d 0b be ca 69 - 0020: 2c 2b 4b 5d b3 ac 67 86 │ 0018: 3f f9 84 40 c5 d5 93 d5 - 0028: 1a 55 40 02 00 00 00 00 │ 0020: 2c 2b 4b 5d b3 ac 67 86 - 0030: 00 00 00 00 00 00 00 00 │ 0028: 1a 55 40 02 00 00 00 00 - 0038: 00 00 00 │ 0030: 00 00 00 00 00 00 00 00 -received 5 bytes │ 0038: 00 00 00 + 0008: 93 c4 c0 13 9f 3f 4f 4a │ 0000: 00 00 00 1d 01 00 00 00 + 0010: fe 97 15 d2 6a 43 95 36 │ 0008: 93 c4 c0 13 9f 3f 4f 4a + 0018: 91 cd c8 61 57 4f fc 81 │ 0010: fe 97 15 d2 6a 43 95 36 + 0020: 96 │ 0018: 91 cd c8 61 57 4f fc 81 +received 54 bytes │ 0020: 96 + 0000: 00 00 00 32 01 00 00 00 │ sent 54 bytes + 0008: f2 1f f2 15 20 be be 5d │ 0000: 00 00 00 32 01 00 00 00 + 0010: 07 c6 81 3b 97 2d e3 61 │ 0008: f2 1f f2 15 20 be be 5d + 0018: 7a 0a 0d 50 a3 6b e3 78 │ 0010: 07 c6 81 3b 97 2d e3 61 + 0020: 4e 9f ec e5 4c ff 8d 80 │ 0018: 7a 0a 0d 50 a3 6b e3 78 + 0028: 32 41 77 41 01 01 00 00 │ 0020: 4e 9f ec e5 4c ff 8d 80 + 0030: 00 93 00 00 00 00 │ 0028: 32 41 77 41 01 01 00 00 +sent 54 bytes │ 0030: 00 93 00 00 00 00 + 0000: 00 00 00 32 01 00 00 00 │ received 54 bytes + 0008: 93 40 1e 39 7b d9 a3 32 │ 0000: 00 00 00 32 01 00 00 00 + 0010: 70 67 e1 41 f1 93 fd 25 │ 0008: 93 40 1e 39 7b d9 a3 32 + 0018: 9d 73 bb 56 ce 1b 4d 5a │ 0010: 70 67 e1 41 f1 93 fd 25 + 0020: 6e 29 1a f9 b8 22 bd 08 │ 0018: 9d 73 bb 56 ce 1b 4d 5a + 0028: 80 42 55 40 41 02 00 00 │ 0020: 6e 29 1a f9 b8 22 bd 08 + 0030: 00 00 00 00 00 00 │ 0028: 80 42 55 40 41 02 00 00 +received 5 bytes │ 0030: 00 00 00 00 00 00 0000: 00 00 00 01 48 │ sent 5 bytes │ 0000: 00 00 00 01 48 diff --git a/tests/stale_floor.rs b/tests/stale_floor.rs index 0fb9e646d..d896e8cc4 100644 --- a/tests/stale_floor.rs +++ b/tests/stale_floor.rs @@ -49,8 +49,8 @@ fn message_minted_after_bootstrap_survives_gossip() { // forgotten" and evict the fresh message from both sides. wire_gossip_async(&f, &b).await; - let f_has = f.snapshot().iter().any(|(_, _, m)| **m == 100); - let b_has = b.snapshot().iter().any(|(_, _, m)| **m == 100); + let f_has = f.snapshot().iter().any(|(_, m)| **m == 100); + let b_has = b.snapshot().iter().any(|(_, m)| **m == 100); assert!( f_has && b_has, "message 100 must survive the sync: f_has={f_has} b_has={b_has}" diff --git a/tests/target_message_size.rs b/tests/target_message_size.rs index 775b9f1c0..78d31a71d 100644 --- a/tests/target_message_size.rs +++ b/tests/target_message_size.rs @@ -53,8 +53,8 @@ fn assert_converges(pair: (Rumors, Rumors)) { let (left, right) = (left.snapshot(), right.snapshot()); assert_eq!(left.len(), right.len()); assert_eq!( - left.iter().map(|(k, _, _)| k).collect::>(), - right.iter().map(|(k, _, _)| k).collect::>(), + left.iter().collect::>(), + right.iter().collect::>(), ); } diff --git a/tests/tradeoff_probe.rs b/tests/tradeoff_probe.rs index ecf42ea21..5af10104f 100644 --- a/tests/tradeoff_probe.rs +++ b/tests/tradeoff_probe.rs @@ -33,12 +33,13 @@ mod latency; use std::time::Duration; -use borsh::{BorshDeserialize, BorshSerialize}; use rand::rngs::SmallRng; use rand::{RngCore, SeedableRng}; use rumors::testing::{envelope_and_wire_bytes, supply_decode_envelope_bytes, window_capacities}; use rumors::{Peer, Protocol, Rumors}; +use serde::Serialize; +use serde::de::DeserializeOwned; /// One-way delay for the virtual-time measurements (the timer grain). const DELAY: Duration = Duration::from_millis(10); @@ -58,7 +59,7 @@ const UNBOUNDED: usize = 8 << 30; fn diverged(budget: usize, mint: &mut impl FnMut(&mut SmallRng) -> T) -> (Rumors, Rumors) where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { let left = Peer::seed().sync_memory_budget(budget).into_rumors(); let mut rng = SmallRng::seed_from_u64(0x0b05_2026_7ade_0ff1); @@ -100,7 +101,7 @@ where /// principle: every wire event lands on an exact delay multiple). fn wire_hops(budget: usize, pipe: usize, mint: &mut impl FnMut(&mut SmallRng) -> T) -> u64 where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { let (left, right) = diverged(budget, mint); let runtime = tokio::runtime::Builder::new_current_thread() @@ -132,7 +133,7 @@ fn run_cells( targets: &[f64], mint: &mut impl FnMut(&mut SmallRng) -> T, ) where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { let (envelope, _) = envelope_and_wire_bytes(); let overhead = 28usize; diff --git a/tests/window_corners.rs b/tests/window_corners.rs index b785e8ac2..18c58ef8e 100644 --- a/tests/window_corners.rs +++ b/tests/window_corners.rs @@ -99,7 +99,7 @@ fn asymmetric_catch_up_is_ladder_bound_at_the_floor() { let measured = hops(pair(0, 1, 20_000, 0)); eprintln!("asymmetric catch-up at budget 0: {measured} hops"); // Ladder hops: the dispute chain prunes within a few levels (the one - // shared key's subtree thins to exactly that leaf and matches), and + // shared message's subtree thins to exactly that leaf and matches), and // the supply is one unidirectional stream. The shape measures 8 // exact hops; a size-priced session would cost ~2 hops per message — // three orders of magnitude past this bound.