From 961f63c6b67da0603e98b9fc1efe9315b2d5836d Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 18:45:34 -0400 Subject: [PATCH 01/11] tree: derive leaf identity from the version alone, detect collisions locally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A leaf's path is now the full-width BLAKE3 hash of its version's canonical bytes, and its Merkle digest commits the version (plus the compressed suffix) instead of resting on a content-derived path: message bytes enter no path and no digest, so every compared quantity is a pure function of the version set. Identity rests on the invariant the protocol already requires everywhere — no two messages ever share a version — instead of on a canonical content encoding, and a content author contributes zero bits to any compared digest (issue #12). What content addressing made silently divergent becomes locally detectable: an insert landing on an occupied path, or a merge meeting two leaves at one path that disagree on version or payload, is now a typed LeafCollision instead of silent split-brain. The error is crate-internal — no input can produce it (a fresh tick strictly dominates the ceiling bounding every live leaf, and ingestion enforces containment), so the public seams expect() it as the invariant breach it would be, and reused-version copies on replicas that never meet at one node remain digest-equal by design: an accepted, modeled trade. Test fixtures that diversified leaves by payload bytes now diversify by version, since payloads no longer move paths. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- src/batch.rs | 5 +- src/conformance/backend.rs | 2 +- src/peer/gossip.rs | 9 +- src/tests.rs | 5 +- src/tree.rs | 84 ++- src/tree/arb.rs | 68 ++- src/tree/mirror/alternating/tests.rs | 4 +- .../streaming/materialized/unknown/tests.rs | 7 +- .../mirror/streaming/remote/adapter/decode.rs | 15 +- .../mirror/streaming/remote/adapter/tests.rs | 8 +- .../remote/adapter/tests/fan_occupancy.rs | 5 +- .../remote/adapter/tests/malformed.rs | 12 +- .../streaming/remote/adapter/tests/parking.rs | 3 +- .../mirror/streaming/remote/proxy/tests.rs | 28 +- .../remote/proxy/tests/declarations.rs | 32 +- .../streaming/remote/proxy/tests/failures.rs | 13 +- .../streaming/remote/proxy/tests/greeting.rs | 45 +- .../streaming/remote/proxy/tests/transport.rs | 13 +- src/tree/mirror/streaming/tests/fixtures.rs | 24 +- src/tree/tests.rs | 517 ++++++++++++------ src/tree/traverse.rs | 2 +- src/tree/traverse/act.rs | 42 +- src/tree/traverse/join.rs | 86 ++- src/tree/traverse/join/tests.rs | 3 +- src/tree/traverse/unknown/tests.rs | 7 +- src/tree/typed/hash.rs | 74 ++- src/tree/typed/hash/tests.rs | 16 +- src/tree/typed/path.rs | 47 +- src/tree/typed/path/tests.rs | 30 +- src/tree/typed/untyped.rs | 15 +- src/tree/typed/untyped/tests.rs | 23 +- 31 files changed, 797 insertions(+), 447 deletions(-) diff --git a/src/batch.rs b/src/batch.rs index 4c21a3654..5fe06cd37 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -105,7 +105,10 @@ impl Drop for Batch<'_, T> { // Notify observers iff the batch changed the tree, straight from // `act`'s changed flag: no root hash is read inside this critical // section (`Tree::act` states the flag's contract). - inner.tree.act(party, actions) + inner.tree.act(party, actions).expect( + "a fresh tick strictly dominates the ceiling, which bounds \ + every live leaf, so a local insert cannot collide", + ) }); } } 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/peer/gossip.rs b/src/peer/gossip.rs index 4e362a6d6..6e62a52c9 100644 --- a/src/peer/gossip.rs +++ b/src/peer/gossip.rs @@ -911,7 +911,14 @@ impl Peer { merged.latest().partial_cmp(inner.tree.latest()), None | Some(std::cmp::Ordering::Greater) ); - let tree_changed = inner.tree.join(merged); + // A leaf collision in the merge is unreachable from any input: + // both trees derive paths from versions locally, so it would + // take a full-width hash collision between distinct versions + // (off-model) or a broken tree invariant in this crate. + let tree_changed = inner + .tree + .join(merged) + .expect("reconciled leaves cannot collide: paths are version-derived"); peer_retiring || tree_changed || ceiling_advancing }); if party_overlap { diff --git a/src/tests.rs b/src/tests.rs index 9e6d33ceb..6f320d989 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -563,7 +563,10 @@ fn uncontained_supply_fails_gossip_and_poisons_the_link() { let (escaped_root, _, escaped) = crate::tree::arb::poisoned_root(&party_of(&poisoned), &base, Message::new(0u64)); poisoned.inner.send_modify(|inner| { - inner.tree.join(Tree { root: escaped_root }); + inner + .tree + .join(Tree { root: escaped_root }) + .expect("collision-free by construction"); }); assert!( !crate::tree::mirror::contained(&escaped, poisoned.inner.borrow().tree.latest()), diff --git a/src/tree.rs b/src/tree.rs index 210b3ae28..955cdb0b2 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 @@ -60,7 +63,7 @@ use std::sync::Arc; mod key; -mod traverse; +pub(crate) mod traverse; pub(crate) mod typed; use crate::{Version, causally, message::Message, tree::typed::Node}; @@ -79,11 +82,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, @@ -395,7 +399,20 @@ 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 + /// + /// # Errors + /// + /// [`traverse::LeafCollision`] if an insert lands on an occupied path + /// disagreeing on version or payload; the tree is untouched. Paths are + /// version-derived and each insert's fresh tick strictly dominates the + /// ceiling bounding every live leaf, so this is unreachable outside a + /// crate bug or an off-model hash collision — callers `expect` it, and + /// it is never user-visible ([`traverse::LeafCollision`]). + pub fn act( + &mut self, + party: &before::Party, + actions: I, + ) -> Result where T: Send + Sync, I: IntoIterator>, @@ -427,7 +444,7 @@ impl Tree { let (key, value) = match action { Action::Forget(hash) => (hash, None), Action::Insert(value) => { - let key = typed::Path::for_leaf(&version, value.bytes()).into(); + let key = typed::Path::for_leaf(&version).into(); (key, Some(value)) } }; @@ -454,8 +471,9 @@ 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. - fn react(&mut self, reactions: I) -> bool + /// movement either: the tree is untouched. Errors exactly as + /// [`act`](Self::act) does, with the tree untouched on `Err`. + fn react(&mut self, reactions: I) -> Result where T: Send + Sync, M: Into>>, @@ -511,9 +529,11 @@ impl Tree { let new_root = traverse::act(self.root.root.clone(), actions, |v: &Version| { new_ceiling |= v; changed = true; - }); + })?; - // The commit point: the walk returned without unwinding. Both fields + // The commit point: the walk returned without unwinding or erroring + // (a leaf-collision error above returns before anything of `self` + // mutates, the same atomicity as an unwind). Both fields // are assigned before the pre-image drops, because that drop runs // user code — everything the batch displaced becomes uniquely held // here, so its cascading `T` destructors run now, and a panicking @@ -522,7 +542,7 @@ impl Tree { let pre_image = std::mem::replace(&mut self.root.root, new_root); self.root.ceiling = new_ceiling; drop(pre_image); - changed + Ok(changed) } /// Merges `other` into `self` by a single simultaneous recursion over @@ -548,7 +568,15 @@ 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 + /// + /// # Errors + /// + /// [`traverse::LeafCollision`] if the two trees hold leaves at one path + /// that disagree on version or payload; this tree is untouched (hash, + /// ceiling, and content all unchanged). Unreachable outside a crate bug + /// or an off-model hash collision — callers `expect` it, and it is + /// never user-visible ([`traverse::LeafCollision`]). + pub fn join(&mut self, other: Tree) -> Result where T: Send + Sync, { @@ -580,20 +608,22 @@ impl Tree { &self.root.ceiling, &their_version, &mut changed, - ); + )?; 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 or erroring (a leaf-collision error above + // returns before anything of `self` mutates, the same atomicity as + // an unwind). 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. let pre_image = std::mem::replace(&mut self.root.root, merged); self.root.ceiling = new_ceiling; drop(pre_image); - changed + Ok(changed) } } diff --git a/src/tree/arb.rs b/src/tree/arb.rs index f6b247578..56a8eba32 100644 --- a/src/tree/arb.rs +++ b/src/tree/arb.rs @@ -75,11 +75,11 @@ 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(); - act(None, actions, |_| ()) + act(None, actions, |_| ()).expect("collision-free by construction") }) .boxed() } @@ -151,18 +151,21 @@ pub fn arb_divergent_pair() -> BoxedStrategy<(crate::tree::Root<()>, crate::tree base.act( &p_s, (0..n_shared).map(|_| Action::Insert(Message::new(()))), - ); + ) + .expect("collision-free by construction"); let shared_keys: Vec<_> = base.iter().map(|(k, _, _)| k).collect(); let side = |party: &Party, n: usize, redact: &[bool]| { let mut t = base.clone(); - t.act(party, (0..n).map(|_| Action::Insert(Message::new(())))); + t.act(party, (0..n).map(|_| Action::Insert(Message::new(())))) + .expect("collision-free by construction"); let forgets: Vec<_> = shared_keys .iter() .zip(redact) .filter_map(|(k, &r)| r.then_some(Action::Forget(*k))) .collect(); - t.act(party, forgets); + t.act(party, forgets) + .expect("collision-free by construction"); t.root }; @@ -209,18 +212,21 @@ pub fn arb_wide_divergent_pair() -> BoxedStrategy<(crate::tree::Root<()>, crate: base.act( &p_s, (0..n_shared).map(|_| Action::Insert(Message::new(()))), - ); + ) + .expect("collision-free by construction"); let shared_keys: Vec<_> = base.iter().map(|(k, _, _)| k).collect(); let side = |party: &Party, n: usize, redact: &[bool]| { let mut t = base.clone(); - t.act(party, (0..n).map(|_| Action::Insert(Message::new(())))); + t.act(party, (0..n).map(|_| Action::Insert(Message::new(())))) + .expect("collision-free by construction"); let forgets: Vec<_> = shared_keys .iter() .zip(redact) .filter_map(|(k, &r)| r.then_some(Action::Forget(*k))) .collect(); - t.act(party, forgets); + t.act(party, forgets) + .expect("collision-free by construction"); t.root }; @@ -260,7 +266,8 @@ pub fn arb_deep_divergent_pair() -> BoxedStrategy<(crate::tree::Root<()>, crate: Action::Insert(Message::new(())), )], |_| (), - ); + ) + .expect("collision-free by construction"); // One side: `width` sibling leaves diverging at `depth`, all on // the side's own party. The branch ranges are disjoint across @@ -280,7 +287,7 @@ pub fn arb_deep_divergent_pair() -> BoxedStrategy<(crate::tree::Root<()>, crate: let node = if leaves.is_empty() { base.clone() } else { - act(base.clone(), leaves, |_| ()) + act(base.clone(), leaves, |_| ()).expect("collision-free by construction") }; root_with_ceiling(node, shared_version.clone() | version) }; @@ -340,7 +347,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() @@ -387,7 +394,8 @@ pub fn early_first_child_dispute_pair() -> (crate::tree::Root<()>, crate::tree:: let build = |party: &Party, base: Version, live: usize| { let mut tree = Tree::new(); tree.root.ceiling = base; - tree.act(party, (0..live).map(|_| Action::Insert(Message::new(())))); + tree.act(party, (0..live).map(|_| Action::Insert(Message::new(())))) + .expect("collision-free by construction"); tree }; let left = build(&p_a, burnt(&p_a, at), LEFT_LEAVES); @@ -458,7 +466,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, @@ -468,7 +476,8 @@ pub fn uncontained_supply_pair() -> (crate::tree::Root<()>, crate::tree::Root<() Action::Insert(receiver_message), )], |_| (), - ), + ) + .expect("collision-free by construction"), receiver_version.clone(), ); @@ -485,13 +494,14 @@ 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, vec![(path, escaped.clone(), Action::Insert(message))], |_| (), - ), + ) + .expect("collision-free by construction"), declared, ); (receiver, poisoned, path, escaped) @@ -541,13 +551,14 @@ 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, vec![(path, escaped.clone(), Action::Insert(message))], |_| (), - ), + ) + .expect("collision-free by construction"), Version::new(), ); (root, path, escaped) @@ -578,7 +589,8 @@ pub fn leaf_parent_dispute_pair() -> ( Action::Insert(Message::new(())), )], |_| (), - ); + ) + .expect("collision-free by construction"); // Each side's extra rides its own disjoint party, so both extras are // causally concurrent with everything else and survive deletion-pruning. @@ -592,7 +604,8 @@ pub fn leaf_parent_dispute_pair() -> ( Action::Insert(Message::new(())), )], |_| (), - ); + ) + .expect("collision-free by construction"); let mut b_version = Version::new(); b_version.tick(&nth_party(2)); @@ -601,9 +614,9 @@ pub fn leaf_parent_dispute_pair() -> ( b_version.clone(), Action::Insert(Message::new(())), ); - let b_node = act(base, vec![b_extra.clone()], |_| ()); + let b_node = act(base, vec![b_extra.clone()], |_| ()).expect("collision-free by construction"); - let union = act(a_node.clone(), vec![b_extra], |_| ()); + let union = act(a_node.clone(), vec![b_extra], |_| ()).expect("collision-free by construction"); let a_ceiling = shared_version.clone() | a_version; let b_ceiling = shared_version | b_version; @@ -639,7 +652,8 @@ pub fn leaf_parent_redaction_pair() -> ( Action::Insert(Message::new(())), )], |_| (), - ); + ) + .expect("collision-free by construction"); // b: built on a's history, inserts a concurrent sibling, then forgets // a's leaf. The forget leaves no tombstone; b remembers only through its @@ -654,16 +668,18 @@ pub fn leaf_parent_redaction_pair() -> ( let mut forget_version = b_version.clone(); forget_version.tick(&nth_party(1)); let b_node = act( - act(a_node.clone(), vec![b_insert.clone()], |_| ()), + act(a_node.clone(), vec![b_insert.clone()], |_| ()) + .expect("collision-free by construction"), vec![( leaf_sibling_path(0x00), forget_version.clone(), Action::Forget, )], |_| (), - ); + ) + .expect("collision-free by construction"); - let survivor = act(None, vec![b_insert], |_| ()); + let survivor = act(None, vec![b_insert], |_| ()).expect("collision-free by construction"); let b_ceiling = a_version.clone() | forget_version; let expected = root_with_ceiling(survivor, a_version.clone() | b_ceiling.clone()); diff --git a/src/tree/mirror/alternating/tests.rs b/src/tree/mirror/alternating/tests.rs index 07acdeb1a..43169eb54 100644 --- a/src/tree/mirror/alternating/tests.rs +++ b/src/tree/mirror/alternating/tests.rs @@ -237,7 +237,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))); } @@ -260,7 +260,7 @@ proptest! { ceiling: actions .iter() .fold(Version::default(), |acc, (_, v, _)| acc | v.clone()), - root: act(None, actions.to_vec(), |_| ()), + root: act(None, actions.to_vec(), |_| ()).expect("collision-free by construction"), }; let tree_a = wrap(&actions_a); diff --git a/src/tree/mirror/streaming/materialized/unknown/tests.rs b/src/tree/mirror/streaming/materialized/unknown/tests.rs index 8e219641f..8756e2120 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 (Option(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 @@ -371,9 +370,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 }); } @@ -498,14 +497,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 +517,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/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..bead58915 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)]); diff --git a/src/tree/mirror/streaming/remote/adapter/tests/parking.rs b/src/tree/mirror/streaming/remote/adapter/tests/parking.rs index 0339a482c..8fd5c6221 100644 --- a/src/tree/mirror/streaming/remote/adapter/tests/parking.rs +++ b/src/tree/mirror/streaming/remote/adapter/tests/parking.rs @@ -77,7 +77,8 @@ fn parked_supply_reply_holds_handles_not_subtrees() { let party = before::Party::seed(); let mut tree = Tree::new(); - tree.act(&party, (0..LEAVES).map(|v| Action::Insert(Message::new(v)))); + tree.act(&party, (0..LEAVES).map(|v| Action::Insert(Message::new(v)))) + .expect("collision-free by construction"); let root = tree .root .root diff --git a/src/tree/mirror/streaming/remote/proxy/tests.rs b/src/tree/mirror/streaming/remote/proxy/tests.rs index 3e41f03e3..b146904c0 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests.rs @@ -274,11 +274,15 @@ async fn equal_versions_return_both_roots() { #[pollster::test] async fn divergent_leaves_converge() { let mut a = Tree::new(); - a.act(&nth_party(0), [Action::Insert(Message::new(()))]); + a.act(&nth_party(0), [Action::Insert(Message::new(()))]) + .expect("collision-free by construction"); let mut b = Tree::new(); - b.act(&nth_party(1), [Action::Insert(Message::new(()))]); + b.act(&nth_party(1), [Action::Insert(Message::new(()))]) + .expect("collision-free by construction"); let mut expected = a.clone(); - expected.join(b.clone()); + expected + .join(b.clone()) + .expect("collision-free by construction"); let (a, b) = reconcile(a.root, b.root).await; assert_eq!(a, expected.root); @@ -290,9 +294,11 @@ async fn divergent_leaves_converge() { #[test] fn symmetric_accept_handshakes_are_live() { let mut a = Tree::new(); - a.act(&nth_party(0), [Action::Insert(Message::new(()))]); + a.act(&nth_party(0), [Action::Insert(Message::new(()))]) + .expect("collision-free by construction"); let mut b = Tree::new(); - b.act(&nth_party(1), [Action::Insert(Message::new(()))]); + b.act(&nth_party(1), [Action::Insert(Message::new(()))]) + .expect("collision-free by construction"); let (a, b) = run_to_quiescence(reconcile_symmetric_accepts(a.root, b.root, 1)) .expect("the production proxy topology became quiescent"); @@ -306,9 +312,11 @@ fn symmetric_accepts_with_distinct_payloads_are_live() { let mut a_party = before::Party::seed(); let b_party = a_party.fork(); let mut a = Tree::new(); - a.act(&a_party, [Action::Insert(Message::new(1_u64))]); + a.act(&a_party, [Action::Insert(Message::new(1_u64))]) + .expect("collision-free by construction"); let mut b = Tree::new(); - b.act(&b_party, [Action::Insert(Message::new(2_u64))]); + b.act(&b_party, [Action::Insert(Message::new(2_u64))]) + .expect("collision-free by construction"); let (a, b) = run_to_quiescence(reconcile_after_preamble(a.root, b.root)) .expect("distinct-payload proxy topology became quiescent"); @@ -535,9 +543,11 @@ fn early_first_child_dispute_is_live() { #[test] fn instrumented_channels_cover_every_proxy_edge() { let mut a = Tree::new(); - a.act(&nth_party(0), [Action::Insert(Message::new(()))]); + a.act(&nth_party(0), [Action::Insert(Message::new(()))]) + .expect("collision-free by construction"); let mut b = Tree::new(); - b.act(&nth_party(1), [Action::Insert(Message::new(()))]); + b.act(&nth_party(1), [Action::Insert(Message::new(()))]) + .expect("collision-free by construction"); let (result, report, trace) = instrumented_reconcile(a.root, b.root, Vec::new()); result.expect("the instrumented wire session should remain live"); trace.assert_valid(); diff --git a/src/tree/mirror/streaming/remote/proxy/tests/declarations.rs b/src/tree/mirror/streaming/remote/proxy/tests/declarations.rs index f37297bfb..ad1c7cc9c 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests/declarations.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests/declarations.rs @@ -38,7 +38,9 @@ fn hash_of(root: &crate::tree::Root<()>) -> [u8; MERKLE_HASH_LEN] { /// The expected reconciled union, computed by the in-memory join oracle. fn union_hash(a: &crate::tree::Root<()>, b: &crate::tree::Root<()>) -> [u8; MERKLE_HASH_LEN] { let mut union = Tree { root: a.clone() }; - union.join(Tree { root: b.clone() }); + union + .join(Tree { root: b.clone() }) + .expect("collision-free by construction"); union.hash() } @@ -47,12 +49,16 @@ fn union_hash(a: &crate::tree::Root<()>, b: &crate::tree::Root<()>) -> [u8; MERK /// initiator election under honest declarations. fn uneven_pair() -> (crate::tree::Root<()>, crate::tree::Root<()>) { let mut small = Tree::new(); - small.act(&nth_party(1), [Action::Insert(Message::new(()))]); + small + .act(&nth_party(1), [Action::Insert(Message::new(()))]) + .expect("collision-free by construction"); let mut large = Tree::new(); - large.act( - &nth_party(0), - (0..4).map(|_| Action::Insert(Message::new(()))), - ); + large + .act( + &nth_party(0), + (0..4).map(|_| Action::Insert(Message::new(()))), + ) + .expect("collision-free by construction"); (small.root, large.root) } @@ -70,12 +76,16 @@ const BULK_MESSAGES: usize = FAN + 1; /// side includes a genuinely batched multi-record run. fn batched_uneven_pair() -> (crate::tree::Root<()>, crate::tree::Root<()>) { let mut small = Tree::new(); - small.act(&nth_party(1), [Action::Insert(Message::new(()))]); + small + .act(&nth_party(1), [Action::Insert(Message::new(()))]) + .expect("collision-free by construction"); let mut large = Tree::new(); - large.act( - &nth_party(0), - (0..BULK_MESSAGES).map(|_| Action::Insert(Message::new(()))), - ); + large + .act( + &nth_party(0), + (0..BULK_MESSAGES).map(|_| Action::Insert(Message::new(()))), + ) + .expect("collision-free by construction"); (small.root, large.root) } diff --git a/src/tree/mirror/streaming/remote/proxy/tests/failures.rs b/src/tree/mirror/streaming/remote/proxy/tests/failures.rs index 8ee6a36e1..48670be90 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests/failures.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests/failures.rs @@ -58,12 +58,15 @@ fn stacked_pair() -> (crate::tree::Root<()>, crate::tree::Root<()>) { left.act( &nth_party(0), (0..8).map(|_| Action::Insert(Message::new(()))), - ); + ) + .expect("collision-free by construction"); let mut right = Tree::new(); - right.act( - &nth_party(1), - (0..8).map(|_| Action::Insert(Message::new(()))), - ); + right + .act( + &nth_party(1), + (0..8).map(|_| Action::Insert(Message::new(()))), + ) + .expect("collision-free by construction"); (left.root, right.root) } diff --git a/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs b/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs index 434ce359a..33d0bdce2 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs @@ -46,7 +46,9 @@ fn wire_reconcile( /// oracle. fn union_hash(a: &crate::tree::Root<()>, b: &crate::tree::Root<()>) -> [u8; MERKLE_HASH_LEN] { let mut union = Tree { root: a.clone() }; - union.join(Tree { root: b.clone() }); + union + .join(Tree { root: b.clone() }) + .expect("collision-free by construction"); union.hash() } @@ -115,19 +117,27 @@ fn order_by_election( fn empty_carried_listing_asks_for_everything() { // The populated responder: one message on party 0. let mut populated = Tree::new(); - populated.act(&nth_party(0), [Action::Insert(Message::new(()))]); + populated + .act(&nth_party(0), [Action::Insert(Message::new(()))]) + .expect("collision-free by construction"); // The emptied initiator: insert-then-forget on party 1 ticks its version // 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(()))]); + emptied + .act(&nth_party(1), [Action::Insert(Message::new(()))]) + .expect("collision-free by construction"); let keys: Vec<_> = emptied.iter().map(|(key, _, _)| key).collect(); - emptied.act(&nth_party(1), keys.into_iter().map(Action::Forget)); + emptied + .act(&nth_party(1), keys.into_iter().map(Action::Forget)) + .expect("collision-free by construction"); assert!(emptied.is_empty(), "the initiator's tree must be empty"); let expected = { let mut union = populated.clone(); - union.join(emptied.clone()); + union + .join(emptied.clone()) + .expect("collision-free by construction"); union }; let (left, right) = wire_reconcile(emptied.root, populated.root.clone()); @@ -148,7 +158,8 @@ fn empty_carried_listing_asks_for_everything() { fn converged_session_carries_listings_unused() { let build = || { let mut tree = Tree::new(); - tree.act(&nth_party(0), [Action::Insert(Message::new(()))]); + tree.act(&nth_party(0), [Action::Insert(Message::new(()))]) + .expect("collision-free by construction"); tree }; let (a, b) = (build(), build()); @@ -185,10 +196,12 @@ fn converged_session_carries_listings_unused() { fn mixed_empty_and_populated_converges() { let empty = Tree::<()>::new(); let mut populated = Tree::new(); - populated.act( - &nth_party(0), - (0..4).map(|_| Action::Insert(Message::new(()))), - ); + populated + .act( + &nth_party(0), + (0..4).map(|_| Action::Insert(Message::new(()))), + ) + .expect("collision-free by construction"); assert_ne!( populated.latest().as_bytes(), empty.latest().as_bytes(), @@ -236,7 +249,8 @@ fn overlapping_sessions_lose_innocent_leaf_after_honored_redaction() { // puts shared runs on both sides of every divergence point. let p = nth_party(0); let mut t0 = Tree::new(); - t0.act(&p, (0..25).map(|_| Action::Insert(Message::new(())))); + t0.act(&p, (0..25).map(|_| Action::Insert(Message::new(())))) + .expect("collision-free by construction"); let keys: Vec<_> = t0.iter().map(|(k, _, _)| k).collect(); // S2's wire session against a peer converged at T0, forked at T0: @@ -251,7 +265,8 @@ fn overlapping_sessions_lose_innocent_leaf_after_honored_redaction() { let mut twin = Tree { root: t0.root.clone(), }; - twin.act(&nth_party(1), [Action::Forget(*k)]); + twin.act(&nth_party(1), [Action::Forget(*k)]) + .expect("collision-free by construction"); // S1's session and install: reconcile T0 against the redacting // twin over the wire, then join the result into the live tree. @@ -263,14 +278,16 @@ fn overlapping_sessions_lose_innocent_leaf_after_honored_redaction() { }; live.join(Tree { root: s1_reconciled, - }); + }) + .expect("collision-free by construction"); let expected = live.hash(); // S2's install, after S1's: joining our own causal past must be an // identity on the tree. live.join(Tree { root: s2_reconciled.clone(), - }); + }) + .expect("collision-free by construction"); if live.hash() != expected { let missing: Vec<_> = keys diff --git a/src/tree/mirror/streaming/remote/proxy/tests/transport.rs b/src/tree/mirror/streaming/remote/proxy/tests/transport.rs index 575b8b350..b0d3a7b17 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests/transport.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests/transport.rs @@ -28,12 +28,15 @@ fn flush_only_one_byte_transport_reconciles() { left.act( &nth_party(0), (0..8).map(|_| Action::Insert(Message::new(()))), - ); + ) + .expect("collision-free by construction"); let mut right = Tree::new(); - right.act( - &nth_party(1), - (0..8).map(|_| Action::Insert(Message::new(()))), - ); + right + .act( + &nth_party(1), + (0..8).map(|_| Action::Insert(Message::new(()))), + ) + .expect("collision-free by construction"); let expected = run_to_quiescence(reconcile_locally(left.root.clone(), right.root.clone())) .expect("materialized oracle should remain live"); let flush_only = plan(1, 1, vec![1; 512], true); diff --git a/src/tree/mirror/streaming/tests/fixtures.rs b/src/tree/mirror/streaming/tests/fixtures.rs index f7613fa28..e03531bfa 100644 --- a/src/tree/mirror/streaming/tests/fixtures.rs +++ b/src/tree/mirror/streaming/tests/fixtures.rs @@ -55,7 +55,7 @@ where Action::Insert(Message::new(value.clone())), )); } - act(node, actions, |_| ()) + act(node, actions, |_| ()).expect("collision-free by construction") } /// Wrap a node as a [`Root`] whose ceiling is the node's own. @@ -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() } @@ -342,7 +349,7 @@ pub(super) fn one_sided_pair(spec: &[(u8, u8, u8)]) -> (Root<()>, Root<()>) { )); } } - let a_node = act(None, shared, |_| ()); + let a_node = act(None, shared, |_| ()).expect("collision-free by construction"); // b's extras: a separate chain on a disjoint party, so they are causally // concurrent with a's version and survive deletion-pruning when provided. @@ -360,7 +367,7 @@ pub(super) fn one_sided_pair(spec: &[(u8, u8, u8)]) -> (Root<()>, Root<()>) { )); } } - let b_node = act(a_node.clone(), extras, |_| ()); + let b_node = act(a_node.clone(), extras, |_| ()).expect("collision-free by construction"); let root = |node: Option>| Root { ceiling: node @@ -431,7 +438,7 @@ pub(super) fn divergent_cells_pair( )); } } - let base_node = act(None, base, |_| ()); + let base_node = act(None, base, |_| ()).expect("collision-free by construction"); // Each side's extras ride their own party's chain, concurrent with the // shared chain and with each other, so both survive deletion-pruning @@ -450,8 +457,9 @@ pub(super) fn divergent_cells_pair( } actions }; - let a_node = act(base_node.clone(), extras(2, a_slot), |_| ()); - let b_node = act(base_node, extras(1, b_slot), |_| ()); + let a_node = + act(base_node.clone(), extras(2, a_slot), |_| ()).expect("collision-free by construction"); + let b_node = act(base_node, extras(1, b_slot), |_| ()).expect("collision-free by construction"); let root = |node: Option>| Root { ceiling: node diff --git a/src/tree/tests.rs b/src/tree/tests.rs index 1963cfa75..719c5a014 100644 --- a/src/tree/tests.rs +++ b/src/tree/tests.rs @@ -3,7 +3,7 @@ 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; @@ -82,15 +82,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 `Key` 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) -> Key { + Path::for_leaf(&version_for(party, scalar)).into() } /// Build a versioned insert triple of the shape `Tree::react` expects: @@ -105,7 +104,7 @@ fn insert_at( scalar: u64, value: Bytes, ) -> (Key, Version, Message) { - (leaf_path(party, scalar, &value), version, msg(value)) + (leaf_path(party, scalar), version, msg(value)) } /// Compute the root hash of the canonical maximally-compressed trie over the @@ -113,12 +112,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 ‖ version` 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 +127,61 @@ 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..]); + preimage.extend_from_slice(version.as_bytes()); + 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 @@ -208,7 +201,8 @@ fn empty_tree_hash_matches_reference() { fn single_value_hash_matches_reference() { let value = Bytes::from(&b"hello"[..]); let mut tree: Tree = Tree::new(); - tree.act(&party_of("P"), [insert_action(value.clone())]); + tree.act(&party_of("P"), [insert_action(value.clone())]) + .expect("collision-free by construction"); let tree_hash = tree.hash(); let reference = reference_hash(&[(version_for("P", 1), value)]); assert_eq!(&tree_hash, reference.as_bytes()); @@ -230,7 +224,7 @@ proptest! { .prop_map(|v| v.into_iter().map(Bytes::from).collect::>()), ) { let mut tree = Tree::new(); - tree.act(&party_of("P"), values.iter().cloned().map(insert_action)); + tree.act(&party_of("P"), values.iter().cloned().map(insert_action)).expect("collision-free by construction"); let reference_input: Vec<_> = values .into_iter() .enumerate() @@ -269,7 +263,7 @@ proptest! { 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).into(); (key, version, message) }; let index_of: HashMap = kept @@ -283,7 +277,7 @@ proptest! { // Route A: one react batch, base order. let mut direct = Tree::new(); - direct.react(kept.iter().map(versioned)); + direct.react(kept.iter().map(versioned)).expect("collision-free by construction"); // Route B: shuffled order, split into two batches, with the extra // leaves inserted in between and redacted again afterwards. @@ -293,20 +287,20 @@ proptest! { .map(|(i, b)| event(kept.len() + i, b)) .collect(); let mut detoured = Tree::new(); - detoured.react(shuffled[..cut].iter().map(versioned)); - detoured.react(extra_events.iter().map(|(k, v, m)| (*k, v.clone(), m.clone()))); - detoured.react(shuffled[cut..].iter().map(versioned)); + detoured.react(shuffled[..cut].iter().map(versioned)).expect("collision-free by construction"); + detoured.react(extra_events.iter().map(|(k, v, m)| (*k, v.clone(), m.clone()))).expect("collision-free by construction"); + detoured.react(shuffled[cut..].iter().map(versioned)).expect("collision-free by construction"); detoured.act( &party_of("P"), extra_events.iter().rev().map(|(k, _, _)| Action::Forget(*k)), - ); + ).expect("collision-free by construction"); // Route C: two disjoint halves, merged in memory. let mut joined = Tree::new(); - joined.react(kept[..cut].iter().map(versioned)); + joined.react(kept[..cut].iter().map(versioned)).expect("collision-free by construction"); let mut right = Tree::new(); - right.react(kept[cut..].iter().map(versioned)); - joined.join(right); + right.react(kept[cut..].iter().map(versioned)).expect("collision-free by construction"); + joined.join(right).expect("collision-free by construction"); let serialize = |tree: &Tree| -> Option> { tree.root @@ -359,27 +353,31 @@ 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)) + .expect("collision-free by construction"); 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); + partitioned.react(batch).expect("collision-free by construction"); } } @@ -399,7 +397,7 @@ proptest! { ) { let mut t_act = Tree::new(); for b in &bytes { - t_act.act(&party_of("P"), [insert_action(b.clone())]); + t_act.act(&party_of("P"), [insert_action(b.clone())]).expect("collision-free by construction"); } let party = "P".to_string(); @@ -413,7 +411,7 @@ proptest! { .into_iter() .zip(bytes.iter().cloned()) .enumerate() - .map(|(i, (v, b))| insert_at(v, &party, (i + 1) as u64, b))); + .map(|(i, (v, b))| insert_at(v, &party, (i + 1) as u64, b))).expect("collision-free by construction"); prop_assert_eq!(t_act.hash(), t_react.hash()); prop_assert_eq!(t_act.latest(), t_react.latest()); @@ -434,7 +432,7 @@ proptest! { if !bytes.is_empty() { tree.act( &party_of("P"), - bytes.iter().cloned().map(insert_action)); + bytes.iter().cloned().map(insert_action)).expect("collision-free by construction"); } let n = bytes.len(); @@ -479,7 +477,7 @@ proptest! { if !bytes.is_empty() { tree.act( &party_of("P"), - bytes.iter().cloned().map(insert_action)); + bytes.iter().cloned().map(insert_action)).expect("collision-free by construction"); } // Forward order is strictly ascending by key. @@ -517,11 +515,11 @@ 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)]); - tree.act(&party_of("P"), [Action::Forget(path)]); + tree.act(&party_of("P"), [insert_action(value)]).expect("collision-free by construction"); + tree.act(&party_of("P"), [Action::Forget(path)]).expect("collision-free by construction"); prop_assert_eq!(tree.hash(), *reference_hash(&[]).as_bytes()); prop_assert_eq!(tree.latest(), version_for(&party, 2)); @@ -535,10 +533,10 @@ 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)]); + tree.act(&party_of("P"), [insert_action(value), Action::Forget(path)]).expect("collision-free by construction"); prop_assert_eq!(tree.hash(), *reference_hash(&[]).as_bytes()); prop_assert_eq!(tree.latest(), Version::new()); @@ -553,16 +551,15 @@ proptest! { nuke in any::(), ) { 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)); let mut t_before = Tree::new(); - t_before.act(&party_of("P"), bytes.into_iter().map(insert_action)); + t_before.act(&party_of("P"), bytes.into_iter().map(insert_action)).expect("collision-free by construction"); let mut t_after = t_before.clone(); - t_after.act(&party_of("P"), [Action::Forget(nuke)]); + t_after.act(&party_of("P"), [Action::Forget(nuke)]).expect("collision-free by construction"); prop_assert_eq!(t_before.hash(), t_after.hash()); prop_assert_eq!(t_before.latest(), t_after.latest()); @@ -587,7 +584,7 @@ proptest! { for i in 0..prior_inserts { tree.act(&party_of(&party), [insert_action(Bytes::from( format!("prior-{i}").into_bytes(), - ))]); + ))]).expect("collision-free by construction"); } let actions: Vec> = (0..batch_size) @@ -595,7 +592,7 @@ proptest! { insert_action(Bytes::from(format!("batch-{i}").into_bytes())) }) .collect(); - tree.act(&party_of(&party), actions); + tree.act(&party_of(&party), actions).expect("collision-free by construction"); // Each prior insert and each batch insert ticks the party once, so the // tree's version is exactly that many ticks of the owning party. @@ -612,10 +609,10 @@ proptest! { for i in 0..prior_batches { tree.act(&party_of("P"), [insert_action(Bytes::from( format!("prior-{i}").into_bytes(), - ))]); + ))]).expect("collision-free by construction"); } let before = tree.latest().clone(); - tree.act(&party_of("P"), std::iter::empty::>()); + tree.act(&party_of("P"), std::iter::empty::>()).expect("collision-free by construction"); prop_assert_eq!(tree.latest(), before); } @@ -623,50 +620,68 @@ 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()).expect("collision-free by construction"); + t_ab.react(batch_b.clone()).expect("collision-free by construction"); 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).expect("collision-free by construction"); + t_ba.react(batch_a).expect("collision-free by construction"); 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. + /// 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()).expect("collision-free by construction"); 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()).expect("collision-free by construction"); + t_twice.react(batch).expect("collision-free by construction"); prop_assert_eq!(t_once, t_twice); } @@ -699,13 +714,13 @@ proptest! { t_base.react(base.iter().cloned().map(|b| { let (v, scalar) = meta_by_value.get(&b).unwrap(); insert_at(v.clone(), &party, *scalar, b) - })); + })).expect("collision-free by construction"); let mut t_shuf = Tree::new(); t_shuf.react(shuffled.iter().cloned().map(|b| { let (v, scalar) = meta_by_value.get(&b).unwrap(); insert_at(v.clone(), &party, *scalar, b) - })); + })).expect("collision-free by construction"); prop_assert_eq!(t_base, t_shuf); } @@ -733,7 +748,7 @@ proptest! { let scalar = (i + 1) as u64; let mut recorded = tree_a.latest().clone(); recorded.tick(&party_of(&a_id)); - tree_a.act(&party_of("A"), [insert_action(value.clone())]); + tree_a.act(&party_of("A"), [insert_action(value.clone())]).expect("collision-free by construction"); a_events.push(insert_at(recorded, &a_id, scalar, value.clone())); } @@ -743,12 +758,12 @@ proptest! { let scalar = (i + 1) as u64; let mut recorded = tree_b.latest().clone(); recorded.tick(&party_of(&b_id)); - tree_b.act(&party_of("B"), [insert_action(value.clone())]); + tree_b.act(&party_of("B"), [insert_action(value.clone())]).expect("collision-free by construction"); b_events.push(insert_at(recorded, &b_id, scalar, value.clone())); } - tree_a.react(b_events.iter().map(|(k, v, m)| (*k, v.clone(), m.clone()))); - tree_b.react(a_events.iter().map(|(k, v, m)| (*k, v.clone(), m.clone()))); + tree_a.react(b_events.iter().map(|(k, v, m)| (*k, v.clone(), m.clone()))).expect("collision-free by construction"); + tree_b.react(a_events.iter().map(|(k, v, m)| (*k, v.clone(), m.clone()))).expect("collision-free by construction"); prop_assert_eq!(tree_a.latest(), tree_b.latest()); prop_assert_eq!(tree_a.hash(), tree_b.hash()); @@ -760,7 +775,7 @@ proptest! { #[test] fn clone_preserves_all_observables(acts in distinct_bytes(8)) { let mut tree = Tree::new(); - tree.act(&party_of("P"), acts.into_iter().map(insert_action)); + tree.act(&party_of("P"), acts.into_iter().map(insert_action)).expect("collision-free by construction"); let cloned = tree.clone(); prop_assert_eq!(cloned.latest(), tree.latest()); @@ -778,9 +793,9 @@ proptest! { #[test] fn eq_implies_same_hash(acts in distinct_bytes(8)) { let mut t1 = Tree::new(); - t1.act(&party_of("P"), acts.iter().cloned().map(insert_action)); + t1.act(&party_of("P"), acts.iter().cloned().map(insert_action)).expect("collision-free by construction"); let mut t2 = Tree::new(); - t2.act(&party_of("P"), acts.into_iter().map(insert_action)); + t2.act(&party_of("P"), acts.into_iter().map(insert_action)).expect("collision-free by construction"); prop_assert_eq!(&t1, &t2); prop_assert_eq!(t1.hash(), t2.hash()); @@ -798,8 +813,8 @@ proptest! { let value = Bytes::from(value); let mut t_a = Tree::new(); let mut t_b = Tree::new(); - t_a.act(&party_of("A"), [insert_action(value.clone())]); - t_b.act(&party_of("B"), [insert_action(value)]); + t_a.act(&party_of("A"), [insert_action(value.clone())]).expect("collision-free by construction"); + t_b.act(&party_of("B"), [insert_action(value)]).expect("collision-free by construction"); prop_assert_ne!(t_a.hash(), t_b.hash()); } @@ -815,11 +830,11 @@ proptest! { let party = "P".to_string(); let value = Bytes::from(value); let mut tree = Tree::new(); - tree.act(&party_of("P"), [insert_action(value.clone())]); - tree.act(&party_of("P"), [insert_action(value.clone())]); + tree.act(&party_of("P"), [insert_action(value.clone())]).expect("collision-free by construction"); + tree.act(&party_of("P"), [insert_action(value.clone())]).expect("collision-free by construction"); - 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()]; @@ -833,7 +848,8 @@ proptest! { #[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(Key([0; 32]))]) + .expect("collision-free by construction"); assert_eq!(tree, Tree::new()); } @@ -1009,7 +1025,7 @@ proptest! { for (i, value) in values.iter().enumerate() { // Rotating parties makes sibling versions concurrent, not just // points on one chain, so branch maxima genuinely compare. - tree.act(&party_of([b'a' + (i % 5) as u8]), [insert_action(value.clone())]); + tree.act(&party_of([b'a' + (i % 5) as u8]), [insert_action(value.clone())]).expect("collision-free by construction"); prop_assert_eq!(tree.max_version_bytes(), naive_max_version_bytes(&tree)); } @@ -1025,7 +1041,7 @@ proptest! { .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)]); + tree.act(&party_of("P"), [Action::Forget(key)]).expect("collision-free by construction"); prop_assert_eq!(tree.max_version_bytes(), naive_max_version_bytes(&tree)); } } @@ -1049,11 +1065,11 @@ proptest! { ) { let mut left: Tree = Tree::new(); for value in &left_values { - left.act(&party_of("A"), [insert_action(value.clone())]); + left.act(&party_of("A"), [insert_action(value.clone())]).expect("collision-free by construction"); } let mut right: Tree = Tree::new(); for value in &right_values { - right.act(&party_of("B"), [insert_action(value.clone())]); + right.act(&party_of("B"), [insert_action(value.clone())]).expect("collision-free by construction"); } // A fork of `right` that `left` first absorbs wholesale: the @@ -1062,7 +1078,7 @@ proptest! { // the deletion-honoring arm, aimed at the argmax half the time // so the resize-down direction is exercised through the merge. let absorbed = right.clone(); - left.join(absorbed); + left.join(absorbed).expect("collision-free by construction"); prop_assert_eq!(left.max_version_bytes(), naive_max_version_bytes(&left)); for forget in forgets { @@ -1078,10 +1094,10 @@ proptest! { else { break; }; - left.act(&party_of("A"), [Action::Forget(key)]); + left.act(&party_of("A"), [Action::Forget(key)]).expect("collision-free by construction"); } - left.join(right); + left.join(right).expect("collision-free by construction"); prop_assert_eq!(left.max_version_bytes(), naive_max_version_bytes(&left)); } } @@ -1113,7 +1129,7 @@ proptest! { tree.act( &party_of("A"), base_values.iter().cloned().map(insert_action), - ); + ).expect("collision-free by construction"); let live: Vec = tree.iter().map(|(k, ..)| k).collect(); let mut actions: Vec> = @@ -1129,7 +1145,7 @@ proptest! { actions.extend(forget_missing.into_iter().map(Action::Forget)); let before = tree.hash(); - let changed = tree.act(&party_of("A"), actions); + let changed = tree.act(&party_of("A"), actions).expect("collision-free by construction"); prop_assert_eq!(changed, tree.hash() != before); } @@ -1151,7 +1167,7 @@ proptest! { ) { let mut tree = Tree { root: a }; let before = tree.hash(); - let changed = tree.join(Tree { root: b }); + let changed = tree.join(Tree { root: b }).expect("collision-free by construction"); prop_assert_eq!(changed, tree.hash() != before); } @@ -1169,7 +1185,7 @@ proptest! { ) { let mut tree = Tree { root: a }; let before = tree.hash(); - let changed = tree.join(Tree { root: b }); + let changed = tree.join(Tree { root: b }).expect("collision-free by construction"); prop_assert_eq!(changed, tree.hash() != before); } } @@ -1188,7 +1204,9 @@ fn deep_divergent_join_changed_flag_is_exact() { for (receiver, counter) in [(a.clone(), b.clone()), (b, a.clone())] { let mut tree = Tree { root: receiver }; let before = tree.hash(); - let changed = tree.join(Tree { root: counter }); + let changed = tree + .join(Tree { root: counter }) + .expect("collision-free by construction"); assert_eq!(changed, tree.hash() != before, "deep gain is biconditional"); assert!(changed, "a deep gain must report changed"); } @@ -1197,7 +1215,9 @@ fn deep_divergent_join_changed_flag_is_exact() { // counterparty has, so the full-depth divergent descent nets nothing. let mut tree = Tree { root: expected }; let before = tree.hash(); - let changed = tree.join(Tree { root: a }); + let changed = tree + .join(Tree { root: a }) + .expect("collision-free by construction"); assert_eq!( changed, tree.hash() != before, @@ -1209,7 +1229,9 @@ fn deep_divergent_join_changed_flag_is_exact() { let (a, b, _survivor) = crate::tree::arb::leaf_parent_redaction_pair(); let mut tree = Tree { root: a }; let before = tree.hash(); - let changed = tree.join(Tree { root: b }); + let changed = tree + .join(Tree { root: b }) + .expect("collision-free by construction"); assert_eq!( changed, tree.hash() != before, @@ -1227,19 +1249,24 @@ fn deep_divergent_join_changed_flag_is_exact() { #[test] fn ceiling_only_join_reports_unchanged() { let mut tree: Tree = Tree::new(); - tree.act(&party_of("A"), [insert_action(Bytes::from_static(b"kept"))]); + tree.act(&party_of("A"), [insert_action(Bytes::from_static(b"kept"))]) + .expect("collision-free by construction"); // The counterparty: a tree that sent one message on its own disjoint // party and then redacted it, leaving no content but an advanced // 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"))]); + other + .act(&party_of("B"), [insert_action(Bytes::from_static(b"gone"))]) + .expect("collision-free by construction"); let key = other .iter() .map(|(k, ..)| k) .next() .expect("one live message"); - other.act(&party_of("B"), [Action::Forget(key)]); + other + .act(&party_of("B"), [Action::Forget(key)]) + .expect("collision-free by construction"); assert!( other.is_empty(), "the counterparty redacted its only message" @@ -1247,7 +1274,7 @@ fn ceiling_only_join_reports_unchanged() { let before = tree.hash(); let ceiling_before = tree.latest().clone(); - let changed = tree.join(other); + let changed = tree.join(other).expect("collision-free by construction"); assert!( !changed, "a merge that teaches the set nothing reports unchanged", @@ -1281,12 +1308,15 @@ fn act_changed_flag_is_conservative_only_in_a_poisoned_store() { let mut tree = Tree { root: receiver }; assert!( - tree.join(Tree { root: poisoned }), + tree.join(Tree { root: poisoned }) + .expect("collision-free by construction"), "planting the escaped leaf is a real change", ); let before = tree.hash(); - let changed = tree.act(&receiver_party, [Action::Forget(key)]); + let changed = tree + .act(&receiver_party, [Action::Forget(key)]) + .expect("collision-free by construction"); assert!( changed, "the skipped forget reports changed: the conservative direction", @@ -1322,7 +1352,8 @@ fn escaped_version_defeats_redaction_in_a_poisoned_store() { // 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 }); + tree.join(Tree { root: poisoned }) + .expect("collision-free by construction"); assert!(tree.get(&key).is_some(), "the join plants the escaped leaf"); assert!( !mirror::contained(&escaped, tree.latest()), @@ -1331,7 +1362,8 @@ fn escaped_version_defeats_redaction_in_a_poisoned_store() { // Redaction is silently skipped: the forget's version ticks from the // ceiling, which the escaped version strictly dominates. - tree.act(&receiver_party, [Action::Forget(key)]); + tree.act(&receiver_party, [Action::Forget(key)]) + .expect("collision-free by construction"); assert!( tree.get(&key).is_some(), "redacting the escaped leaf is silently skipped", @@ -1341,7 +1373,7 @@ fn escaped_version_defeats_redaction_in_a_poisoned_store() { // receives it on merge, because no ceiling ever classifies it as // already-seen-and-deleted. let mut fresh: Tree<()> = Tree::new(); - fresh.join(tree); + fresh.join(tree).expect("collision-free by construction"); assert!( fresh.get(&key).is_some(), "the escaped leaf re-plants into a fresh replica", @@ -1394,7 +1426,8 @@ mod span_door_traffic { let (equal, empty, comparable, concurrent) = cells(|| { let mut tree: Tree = Tree::new(); for round in 0..8 { - tree.act(&party_of("A"), batch("a", round, 64).map(insert_action)); + tree.act(&party_of("A"), batch("a", round, 64).map(insert_action)) + .expect("collision-free by construction"); tree.warm_caches(); } }); @@ -1426,17 +1459,20 @@ mod span_door_traffic { for label in ["A", "B", "C", "D"] { let mut tree: Tree = Tree::new(); for round in 0..4 { - tree.act(&party_of(label), batch(label, round, 32).map(insert_action)); + tree.act(&party_of(label), batch(label, round, 32).map(insert_action)) + .expect("collision-free by construction"); } tree.warm_caches(); - merged.join(tree); + merged.join(tree).expect("collision-free by construction"); } merged.warm_caches(); // Incremental rounds on the merged tree: acts invalidate // ancestor memos, so re-warming re-folds them against the // merged population. for round in 100..104 { - merged.act(&party_of("A"), batch("a", round, 32).map(insert_action)); + merged + .act(&party_of("A"), batch("a", round, 32).map(insert_action)) + .expect("collision-free by construction"); merged.warm_caches(); } }); @@ -1473,7 +1509,8 @@ fn act_unwind_leaves_tree_byte_identical() { tree.act( &party_of("P"), [insert_action(Bytes::from_static(b"survivor"))], - ); + ) + .expect("collision-free by construction"); let hash_before = tree.hash(); let ceiling_before = tree.latest().clone(); assert!(!tree.is_empty()); @@ -1487,7 +1524,8 @@ fn act_unwind_leaves_tree_byte_identical() { panic!("injected: actions iterator panics mid-drain") })); let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - tree.act(&party_of("P"), panicking_actions); + tree.act(&party_of("P"), panicking_actions) + .expect("collision-free by construction"); })); assert!(unwound.is_err(), "the injected panic must unwind out"); @@ -1525,13 +1563,16 @@ fn join_unwind_leaves_tree_byte_identical() { ours.act( &party_of("A"), [b"ours-1" as &[u8], b"ours-2", b"ours-3"].map(|b| insert_action(Bytes::from_static(b))), - ); + ) + .expect("collision-free by construction"); let mut theirs: Tree = Tree::new(); - theirs.act( - &party_of("B"), - [b"theirs-1" as &[u8], b"theirs-2", b"theirs-3"] - .map(|b| insert_action(Bytes::from_static(b))), - ); + theirs + .act( + &party_of("B"), + [b"theirs-1" as &[u8], b"theirs-2", b"theirs-3"] + .map(|b| insert_action(Bytes::from_static(b))), + ) + .expect("collision-free by construction"); let hash_before = ours.hash(); let ceiling_before = ours.latest().clone(); assert!(!ours.is_empty()); @@ -1541,7 +1582,7 @@ fn join_unwind_leaves_tree_byte_identical() { // already merged into the root frame's copied fan. let _fuse = super::panic_injection::arm(3); let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - ours.join(theirs); + ours.join(theirs).expect("collision-free by construction"); })); assert!( unwound.is_err(), @@ -1613,7 +1654,8 @@ fn act_mid_walk_unwind_leaves_tree_byte_identical() { tree.act( &party_of("A"), [b"held-1" as &[u8], b"held-2", b"held-3"].map(|b| insert_action(Bytes::from_static(b))), - ); + ) + .expect("collision-free by construction"); let hash_before = tree.hash(); let ceiling_before = tree.latest().clone(); assert!(!tree.is_empty()); @@ -1628,7 +1670,8 @@ fn act_mid_walk_unwind_leaves_tree_byte_identical() { tree.act( &party_of("A"), [b"new-1" as &[u8], b"new-2", b"new-3"].map(|b| insert_action(Bytes::from_static(b))), - ); + ) + .expect("collision-free by construction"); })); assert!( unwound.is_err(), @@ -1663,8 +1706,9 @@ 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(); - tree.react([(key, version_for("A", 2), existing)]); + let key: Key = Path::for_leaf(&version_for("A", 2)).into(); + tree.react([(key, version_for("A", 2), existing)]) + .expect("collision-free by construction"); let hash_before = tree.hash(); let ceiling_before = tree.latest().clone(); @@ -1675,7 +1719,8 @@ fn act_destructor_unwind_leaves_tree_byte_identical() { // last handle — mid-walk. let bomb = Message::new(DropBomb { armed: true }); let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - tree.react([(key, version_for("A", 1), bomb)]); + tree.react([(key, version_for("A", 1), bomb)]) + .expect("collision-free by construction"); })); let payload = unwound.expect_err("the armed destructor must unwind out of the apply walk"); assert_eq!( @@ -1717,19 +1762,22 @@ fn join_destructor_unwind_leaves_tree_byte_identical() { ours.act( &party_of("A"), [Action::Insert(Message::new(DropBomb { armed: false }))], - ); + ) + .expect("collision-free by construction"); let bomb = Message::new(DropBomb { armed: true }); // 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(); - ours.act(&party_of("A"), [Action::Insert(bomb)]); + let bomb_key: Key = Path::for_leaf(&version_for("A", 2)).into(); + ours.act(&party_of("A"), [Action::Insert(bomb)]) + .expect("collision-free by construction"); // The counterparty forks while the bomb is live: the clone shares our // nodes (no `T` code runs), and after our forget below releases our // handles, the counterparty holds the bomb's only ones. let theirs = ours.clone(); - ours.act(&party_of("A"), [Action::Forget(bomb_key)]); + ours.act(&party_of("A"), [Action::Forget(bomb_key)]) + .expect("collision-free by construction"); let hash_before = ours.hash(); let ceiling_before = ours.latest().clone(); @@ -1739,7 +1787,7 @@ fn join_destructor_unwind_leaves_tree_byte_identical() { // (A at 3) and we lack its content, so deletion honoring drops the // incoming leaf mid-walk: the last handle, the armed destructor. let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - ours.join(theirs); + ours.join(theirs).expect("collision-free by construction"); })); let payload = unwound.expect_err("the armed destructor must unwind out of the merge walk"); assert_eq!( @@ -1758,3 +1806,132 @@ fn join_destructor_unwind_leaves_tree_byte_identical() { "the ceiling is unchanged: no partial advance escapes the unwind" ); } + +/// `Tree::join` halts with a `LeafCollision` when the two trees bind one +/// path to *different versions* — the shape only a full-width path-hash +/// collision (or a crate bug) can produce — and leaves the receiving tree +/// untouched. +/// +/// The colliding pair is planted directly through `react` at a synthetic +/// shared path, which no public insert can mint; the leaf digest commits +/// the version, so the merge walk descends to the pair instead of pruning +/// it as equal. +#[test] +fn join_detects_a_version_collision_at_one_path() { + let shared: Key = Key::from([0x42; 32]); + + let mut ours: Tree = Tree::new(); + ours.react([(shared, version_for("A", 1), msg(Bytes::from_static(b"a")))]) + .expect("first insert at a fresh path cannot collide"); + + let mut theirs: Tree = Tree::new(); + theirs + .react([(shared, version_for("B", 1), msg(Bytes::from_static(b"b")))]) + .expect("first insert at a fresh path cannot collide"); + + let hash_before = ours.hash(); + let ceiling_before = ours.latest().clone(); + let collision = ours + .join(theirs) + .expect_err("distinct versions at one path must halt the merge"); + // The diagnostic names the resident leaf by its version-derived path + // (the synthetic location itself is not recoverable at the leaf level). + assert_eq!( + collision.path, + <[u8; 32]>::from(Path::for_leaf(&version_for("A", 1))) + ); + assert_eq!(ours.hash(), hash_before, "the tree is untouched on Err"); + assert_eq!( + ours.latest(), + &ceiling_before, + "the ceiling is untouched on Err" + ); +} + +/// Two leaves carrying the *same version* with different payloads compare +/// digest-equal — digests are content-blind — so `Tree::join` keeps one +/// side and reports no change: the modeled trade, pinned so its boundary +/// with the detected (version-mismatch) case stays explicit. +#[test] +fn join_prunes_same_version_payload_divergence_as_equal() { + let version = version_for("A", 1); + let path: Key = Path::for_leaf(&version).into(); + + let mut ours: Tree = Tree::new(); + ours.react([(path, version.clone(), msg(Bytes::from_static(b"ours")))]) + .expect("first insert at a fresh path cannot collide"); + let mut theirs: Tree = Tree::new(); + theirs + .react([(path, version, msg(Bytes::from_static(b"theirs")))]) + .expect("first insert at a fresh path cannot collide"); + + let hash_before = ours.hash(); + let changed = ours + .join(theirs) + .expect("digest-equal leaves prune before the leaf arm"); + 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: Key = Path::for_leaf(&version).into(); + let message = msg(Bytes::from_static(b"same")); + + let mut tree: Tree = Tree::new(); + tree.react([(path, version.clone(), message.clone())]) + .expect("first insert at a fresh path cannot collide"); + let hash_before = tree.hash(); + tree.react([(path, version, message)]) + .expect("a byte-identical re-insert is idempotent"); + 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: `react` halts with a `LeafCollision` +/// naming the path, and the tree is untouched. +#[test] +fn react_detects_version_reuse_at_an_occupied_path() { + let version = version_for("A", 1); + let path: Key = Path::for_leaf(&version).into(); + + let mut tree: Tree = Tree::new(); + tree.react([(path, version.clone(), msg(Bytes::from_static(b"first")))]) + .expect("first insert at a fresh path cannot collide"); + let hash_before = tree.hash(); + let collision = tree + .react([(path, version, msg(Bytes::from_static(b"second")))]) + .expect_err("a second payload under one version must halt the apply"); + assert_eq!(collision.path, <[u8; 32]>::from(path)); + assert_eq!(tree.hash(), hash_before, "the tree is untouched on Err"); +} + +/// An insert landing on a live leaf whose version *differs* (a synthetic +/// path collision) halts with a `LeafCollision` too: both legs of the +/// identity check are enforced, not just payload equality. +#[test] +fn react_detects_a_path_collision_between_distinct_versions() { + let shared: Key = Key::from([0x24; 32]); + + let mut tree: Tree = Tree::new(); + tree.react([(shared, version_for("A", 1), msg(Bytes::from_static(b"a")))]) + .expect("first insert at a fresh path cannot collide"); + let collision = tree + .react([(shared, version_for("B", 1), msg(Bytes::from_static(b"a")))]) + .expect_err("a distinct version at an occupied path must halt the apply"); + // The diagnostic names the incoming insert by its version-derived path. + assert_eq!( + collision.path, + <[u8; 32]>::from(Path::for_leaf(&version_for("B", 1))) + ); +} diff --git a/src/tree/traverse.rs b/src/tree/traverse.rs index 3cab05cfc..2815b4d9f 100644 --- a/src/tree/traverse.rs +++ b/src/tree/traverse.rs @@ -16,4 +16,4 @@ pub use act::{Action, act}; pub(crate) mod unknown; mod join; -pub use join::join; +pub use join::{LeafCollision, join}; diff --git a/src/tree/traverse/act.rs b/src/tree/traverse/act.rs index 990db4562..0a4716776 100644 --- a/src/tree/traverse/act.rs +++ b/src/tree/traverse/act.rs @@ -2,6 +2,7 @@ use itertools::Itertools; use crate::{Version, message::Message}; +use super::join::LeafCollision; use super::typed::*; use height::{Height, Root, S, Z}; @@ -23,11 +24,18 @@ 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. +/// +/// # Errors +/// +/// [`LeafCollision`] if an insert lands on a live leaf disagreeing with it +/// on version or payload (unreachable from any input; see +/// [`LeafCollision`]). On `Err` nothing has been published: the caller's +/// commit point is never reached. pub fn act( node: Option>, actions: I, mut on_action: F, -) -> Option> +) -> Result>, LeafCollision> where T: Send + Sync, F: FnMut(&Version), @@ -55,7 +63,7 @@ pub trait Act: Height { node: Option>, actions: I, on_action: &mut F, - ) -> Option> + ) -> Result>, LeafCollision> where T: Send + Sync, F: FnMut(&Version), @@ -70,7 +78,7 @@ where node: Option>>, actions: I, on_action: &mut F, - ) -> Option>> + ) -> Result>>, LeafCollision> where T: Send + Sync, F: FnMut(&Version), @@ -127,13 +135,15 @@ where continue; } - if let Some(child) = Act::act(existing_child, actions, on_action) { + if let Some(child) = Act::act(existing_child, actions, on_action)? { updated.push((radix, child)); } } // Re-assemble: updated children + untouched existing children. - Node::branch(updated.into_iter().chain(existing_children).collect()) + Ok(Node::branch( + updated.into_iter().chain(existing_children).collect(), + )) } } @@ -142,7 +152,7 @@ impl Act for Z { mut node: Option>, actions: I, on_action: &mut F, - ) -> Option> + ) -> Result>, LeafCollision> where T: Send + Sync, F: FnMut(&Version), @@ -170,6 +180,24 @@ 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); any mismatch is a + // `LeafCollision` — unreachable except through a crate bug or + // an off-model hash collision (see `LeafCollision`), and + // errored before anything commits. + if let (Action::Insert(value), Some(existing)) = (&action, &node) { + if existing.ceiling() != &version + || existing.message().as_slice() != value.as_slice() + { + return Err(LeafCollision { + path: Path::for_leaf(&version).into(), + }); + } + continue; + } + // Set the node node = match action { Action::Forget => None, @@ -184,6 +212,6 @@ impl Act for Z { _ => on_action(&greatest_version), } - node + Ok(node) } } diff --git a/src/tree/traverse/join.rs b/src/tree/traverse/join.rs index 9027867da..e3b9943da 100644 --- a/src/tree/traverse/join.rs +++ b/src/tree/traverse/join.rs @@ -18,9 +18,9 @@ //! 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 (hashes commit shape and versions), 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 @@ -40,6 +40,26 @@ use super::typed::*; use super::unknown::Unknown; use height::{Height, Root, S, Z}; +/// Crate-internal: two live leaves met at one tree path while disagreeing +/// on version or payload. +/// +/// Never user-visible, because no input can produce it: paths are +/// full-width hashes of versions, live leaf versions never exceed the +/// ceiling a fresh tick strictly dominates, and ingestion enforces +/// containment — so a collision requires a bug in this crate or a +/// full-width hash collision (off-model). The traversals return it as a +/// typed error so tests can construct and observe the detector directly; +/// the public seams (`Batch`'s drop commit, the gossip commit) `expect` it +/// away as the invariant breach it is. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +#[error("two distinct leaves collided at one tree path: a version was reused")] +pub struct LeafCollision { + /// The 32-byte version-derived path naming one colliding leaf: the + /// resident leaf's in the merge walk, the incoming insert's in the + /// apply walk. + pub path: [u8; 32], +} + /// Merges two trees rooted at `a` and `b` into one. /// /// `a_version` / `b_version` are the two roots' version vectors, used to honor @@ -51,16 +71,23 @@ 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. +/// +/// # Errors +/// +/// [`LeafCollision`] if two leaves meet at one path while disagreeing on +/// version or payload (unreachable from any input; see [`LeafCollision`]). +/// On `Err`, `changed` may have been set but nothing has been published: +/// the caller's commit point is never reached. pub fn join( a: Option>, b: Option>, a_version: &Version, b_version: &Version, changed: &mut bool, -) -> Option> +) -> Result>, LeafCollision> where T: Send + Sync, { @@ -87,7 +114,7 @@ pub trait Join: Unknown { a_version: &Version, b_version: &Version, changed: &mut bool, - ) -> Option> + ) -> Result>, LeafCollision> where T: Send + Sync; } @@ -102,7 +129,7 @@ where a_version: &Version, b_version: &Version, changed: &mut bool, - ) -> Option>> + ) -> Result>>, LeafCollision> where T: Send + Sync, { @@ -112,7 +139,7 @@ where #[cfg(test)] crate::tree::panic_injection::fire_if_armed(); - match (a, b) { + Ok(match (a, b) { (None, None) => None, // Asymmetric cases: a subtree one side holds and the other lacks. // Filter it against the *other* side's version vector to honor @@ -136,12 +163,11 @@ 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); + return Ok(Some(ours)); } // Differing subtrees: descend one level, merge-walking the @@ -190,7 +216,7 @@ where continue; } - match Join::join(our_child, their_child, a_version, b_version, changed) { + match Join::join(our_child, their_child, a_version, b_version, changed)? { Some(child) => { merged.insert(radix, child); } @@ -202,7 +228,7 @@ where Node::branch(merged) } - } + }) } } @@ -213,11 +239,11 @@ impl Join for Z { a_version: &Version, b_version: &Version, changed: &mut bool, - ) -> Option> + ) -> Result>, LeafCollision> where T: Send + Sync, { - match (a, b) { + Ok(match (a, b) { (None, None) => None, // The leaf-level base of the asymmetric arms' change detection: // our leaf dropped by deletion honoring is a change, and their @@ -232,12 +258,26 @@ 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 path are the same leaf: the path + // is the full-width hash of the version (`Path::for_leaf`), so + // one path is one version, and one version is one message. + // Verify both legs instead of assuming them; a mismatch is a + // `LeafCollision`, unreachable except through a crate bug or an + // off-model hash collision (see `LeafCollision`), and halting + // beats silently keeping a side. (A same-version pair with + // different payloads digests equal and prunes above — digests + // are content-blind by design, a modeled trade.) + (Some(ours), Some(theirs)) => { + if ours.ceiling() != theirs.ceiling() + || ours.message().as_slice() != theirs.message().as_slice() + { + return Err(LeafCollision { + path: Path::for_leaf(ours.ceiling()).into(), + }); + } + Some(ours) + } + }) } } diff --git a/src/tree/traverse/join/tests.rs b/src/tree/traverse/join/tests.rs index ec464b8b0..1f49ceac5 100644 --- a/src/tree/traverse/join/tests.rs +++ b/src/tree/traverse/join/tests.rs @@ -26,7 +26,8 @@ fn mirror_merge(a: Root<()>, b: Root<()>) -> Root<()> { /// Merges via `Tree::join`. fn join_tree(a: Root<()>, b: Root<()>) -> Root<()> { let mut a = Tree { root: a }; - a.join(Tree { root: b }); + a.join(Tree { root: b }) + .expect("collision-free by construction"); a.root } diff --git a/src/tree/traverse/unknown/tests.rs b/src/tree/traverse/unknown/tests.rs index b4ea6e4d8..7c628c317 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(); @@ -111,7 +111,10 @@ fn wide_divergence( } } - (act(None, actions, |_| ()), known) + ( + act(None, actions, |_| ()).expect("collision-free by construction"), + known, + ) } /// `body`'s result with its scanned-bits reading, on a fresh counter. diff --git a/src/tree/typed/hash.rs b/src/tree/typed/hash.rs index 1be67ac73..84eb58336 100644 --- a/src/tree/typed/hash.rs +++ b/src/tree/typed/hash.rs @@ -59,9 +59,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. @@ -86,28 +88,40 @@ impl Hash { } /// The hash of a leaf observed from the top of its compressed `suffix`: - /// `blake3(LEAF_TAG ‖ suffix_len ‖ suffix)`. + /// `blake3(LEAF_TAG ‖ suffix_len ‖ suffix ‖ version)`. /// /// `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. + /// path). `version` is the leaf's version in its canonical encoding, + /// which is self-delimiting, so the preimage stays injective with the + /// suffix length-tagged and the version last. A leaf commits its path + /// bytes and its version — never its message bytes: every digest the + /// mirror compares is a pure function of the version set, so no author + /// of message *content* contributes a single bit to any compared + /// quantity. The path is itself the full-width hash of the version + /// (leaves are version-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; committing the version bytes here as well + /// makes two leaves whose *distinct* versions collided into one path + /// (a full-width hash collision, off-model) digest-unequal, so the + /// merge walk surfaces that impossibility as a local violation instead + /// of silently keeping one side. /// /// # Panics /// /// Panics if `suffix` exceeds 255 bytes. Unreachable through the typed /// tree, whose height cap bounds compressed spans at the 32-byte path. - pub fn leaf(suffix: &[u8]) -> Self { + pub fn leaf(suffix: &[u8], version: &crate::Version) -> Self { + let version = version.as_bytes(); let suffix_len = u8::try_from(suffix.len()).expect("a compressed span fits in one length byte"); - let mut buf = Vec::with_capacity(2 + suffix.len()); + let mut buf = Vec::with_capacity(2 + suffix.len() + version.len()); buf.push(LEAF_TAG); buf.push(suffix_len); buf.extend_from_slice(suffix); + buf.extend_from_slice(version); Hash::of(&buf) } @@ -235,16 +249,14 @@ 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. /// /// 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 +/// 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 `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. @@ -279,29 +291,5 @@ impl From for [u8; 32] { } } -/// 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..4adfb3b7b 100644 --- a/src/tree/typed/hash/tests.rs +++ b/src/tree/typed/hash/tests.rs @@ -27,15 +27,16 @@ fn branch_preimage_layout() { assert_eq!(Hash::branch(&prefix, children), Hash::of(&expected)); } -/// A leaf commits to exactly `LEAF_TAG ‖ suffix_len ‖ suffix` — its -/// compressed suffix, length-tagged, and nothing else. +/// A leaf commits to exactly `LEAF_TAG ‖ suffix_len ‖ suffix ‖ version` — +/// its compressed suffix, length-tagged, then the version's canonical +/// bytes, and never any message bytes. #[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 version = crate::Version::try_from(5).expect("a small scalar version is valid"); + let mut expected = vec![LEAF_TAG, 4, 0x01, 0x02, 0x03, 0x04]; + expected.extend_from_slice(version.as_bytes()); + assert_eq!(Hash::leaf(&suffix, &version), Hash::of(&expected)); } /// The empty tree hashes as a prefixless branch with no children — @@ -52,7 +53,8 @@ fn empty_root_is_the_empty_branch() { /// load-bearing under the single-preimage rule. #[test] fn empty_suffix_leaf_is_not_the_empty_root() { - assert_ne!(Hash::leaf(&[]), Hash::empty_root()); + let version = crate::Version::new(); + assert_ne!(Hash::leaf(&[], &version), Hash::empty_root()); } /// A prefix byte cannot masquerade as child-record bytes: two branches whose diff --git a/src/tree/typed/path.rs b/src/tree/typed/path.rs index f03fa7e62..143842c60 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::ContentHash; use super::height::{Height, Root, S}; use crate::Version; @@ -18,33 +18,30 @@ pub struct Path { } impl Path { - /// Get a path for the given leaf, incorporating its version and value. + /// Get a 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()); - + /// The version alone determines where a leaf lives. Its canonical + /// [`as_bytes`](Version::as_bytes) is unique per insert: every + /// [`tick`](Version::tick) yields a distinct canonical encoding (local + /// uniqueness), and parties descend from a shared seed by disjoint + /// forks, so no two parties ever stamp the same version (global + /// uniqueness by party disjointness). Those are invariants the + /// protocol already rests on everywhere, so deriving identity from the + /// version adds no new assumption — and it keeps message bytes out of + /// every path and digest, so no actor can steer where anything lands by + /// choosing content. + /// + /// The path is the full-width 32-byte `ContentHash`, never the + /// truncated Merkle `Hash`: a path collision is permanent split-brain + /// (see `ContentHash`), so identity gets the full width even though the + /// comparison digests are narrower. The preimage is one canonical byte + /// string (self-delimiting, no second component), so no + /// length-extension or concatenation ambiguity arises. + pub fn for_leaf(version: &Version) -> Self { Self { height: PhantomData, - hash: hasher.finalize().into(), + hash: ContentHash::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/untyped.rs b/src/tree/typed/untyped.rs index 1fd2be264..081eef6b5 100644 --- a/src/tree/typed/untyped.rs +++ b/src/tree/typed/untyped.rs @@ -445,8 +445,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) } @@ -471,7 +471,7 @@ impl Node { // path). let prefix: ArrayVec<[u8; 32]> = self.inner.prefix.iter().rev().copied().collect(); match &self.inner.children { - Children::Leaf { .. } => Hash::leaf(&prefix), + Children::Leaf { version, .. } => Hash::leaf(&prefix, version), Children::Branch { children, .. } => Hash::branch( &prefix, children.iter().map(|(radix, child)| (radix, child.hash())), @@ -747,8 +747,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/tests.rs b/src/tree/typed/untyped/tests.rs index 2e2cb44d0..75aec57cb 100644 --- a/src/tree/typed/untyped/tests.rs +++ b/src/tree/typed/untyped/tests.rs @@ -493,7 +493,7 @@ proptest! { /// always has >= 2 children, by the path-compression invariant), so its /// index accumulates onto the reference prefix in path order until the /// underlying leaf or true branch point is reached. The preimage is then -/// assembled by hand — `LEAF_TAG ‖ len ‖ prefix` for a leaf, +/// assembled by hand — `LEAF_TAG ‖ len ‖ prefix ‖ version` for a leaf, /// `BRANCH_TAG ‖ len ‖ prefix ‖ count(u16 BE) ‖ (radix ‖ hash)*` for a /// branch — with every child hash computed by the same reference /// recursively, never by [`Node::hash`]. @@ -503,9 +503,10 @@ fn reference_hash(mut node: Node<()>) -> super::Hash { let mut prefix: Vec = Vec::new(); loop { node = match node.into_children() { - Err(_leaf) => { + Err(leaf) => { let mut buf = vec![LEAF_TAG, u8::try_from(prefix.len()).expect("short prefix")]; buf.extend_from_slice(&prefix); + buf.extend_from_slice(leaf.ceiling().as_bytes()); return super::Hash::of(&buf); } Ok(children) if children.len() == 1 => { @@ -546,9 +547,9 @@ fn full_depth_paths() -> impl Strategy> { /// The canonical tree over `paths` observed from `depth`, built from /// scratch by the maximally-compressing bulk constructor. /// -/// Leaf versions are all genesis: the hash convention never commits a -/// version, so varying them adds nothing to the hash properties checked -/// against this reference. +/// Leaf versions are all genesis: each leaf preimage commits the version's +/// canonical bytes as a constant tail here, so the shape properties under +/// test are isolated from version variation. fn canonical_at(depth: usize, paths: &[[u8; 32]]) -> Node<()> { let mut entries: Vec<([u8; 32], Option>)> = paths .iter() @@ -595,14 +596,17 @@ 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]),); + let mut preimage = vec![LEAF_TAG, 2, 0xBB, 0xAA]; + preimage.extend_from_slice(Version::new().as_bytes()); + assert_eq!(wrapped.hash(), super::Hash::of(&preimage)); } /// A hand-built two-leaf tree pins the preimages end to end. /// -/// Each leaf commits its length-tagged 29-byte suffix, and the root branch -/// commits its 2-byte shared prefix in path order, the big-endian `u16` -/// child count, and both ascending `radix ‖ hash` records. +/// Each leaf commits its length-tagged 29-byte suffix and its (genesis) +/// version's canonical bytes, and the root branch commits its 2-byte shared +/// prefix in path order, the big-endian `u16` child count, and both +/// ascending `radix ‖ hash` records. #[test] fn small_tree_hash_matches_byte_literal_preimage() { const LEAF_TAG: u8 = 0; @@ -617,6 +621,7 @@ fn small_tree_hash_matches_byte_literal_preimage() { let leaf_hash = |suffix: &[u8]| { let mut buf = vec![LEAF_TAG, u8::try_from(suffix.len()).expect("short suffix")]; buf.extend_from_slice(suffix); + buf.extend_from_slice(Version::new().as_bytes()); super::Hash::of(&buf) }; // Root: prefix [1, 2] (path order), two children at radixes 3 and 7, From 9c73d7b463ee886c27b549640597b925aa9ff0e9 Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 19:35:48 -0400 Subject: [PATCH 02/11] api: retire Key; a message's public identity is its Version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version-only leaf addressing leaves Key nothing to add: it named the same 32 bytes the version already determines. The public surface retargets — Snapshot::get and Rumors::redact take &Version, snapshot iteration and both observers yield versions (already their identity) without a separate key, and persisting an identity for later redaction means persisting the version's canonical bytes. Internally, iterators that reconstructed paths for no remaining consumer stop doing so (the borrowed walks slim their frontier frames), while the owned walk keeps yielding paths for the mirror's leaf keying, and the causal observer stages its backlog by (Rank, canonical version bytes): rank cached once per leaf for cheap repeated comparison, byte tiebreak identical to before::Ranked's total order. The reconciliation docs re-derive their identity and digest-width arguments for version addressing: compared digests are pure functions of the version set, so the offline content-grinding vector is structurally gone rather than priced, and 24 bytes remains the unconditional birthday floor against actors with version-minting influence. Test fixtures that steered tree shapes by payload search can no longer do so; the searched-shape fixtures in the snapshot suites are re-staged in the wire-format re-acceptance. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- AGENTS.md | 4 +- benches/gossip_fixed.rs | 28 +-- benches/in_memory.rs | 40 ++-- benches/support/grid.rs | 20 +- examples/swarm.rs | 114 +++++------ examples/swarm/tests.rs | 20 +- src/batch.rs | 13 +- src/lib.rs | 9 +- src/peer.rs | 6 +- src/peer/gossip/tests.rs | 10 +- src/reconciliation.rs | 95 +++++---- src/rumors.rs | 62 +++--- src/rumors/causal.rs | 44 +++-- src/rumors/unordered.rs | 40 ++-- src/snapshot.rs | 20 +- src/tree.rs | 69 +++---- src/tree/arb.rs | 9 +- src/tree/key.rs | 72 ------- .../streaming/remote/proxy/tests/greeting.rs | 20 +- src/tree/tests.rs | 185 ++++++++++-------- src/tree/typed/hash.rs | 31 ++- src/tree/typed/node.rs | 5 +- src/tree/typed/path.rs | 26 +-- src/tree/typed/prefix.rs | 12 +- src/tree/typed/untyped/iter.rs | 142 +++++--------- src/tutorial.rs | 32 +-- tests/async_wire.rs | 6 +- tests/bookmark_causality.rs | 12 +- tests/bookmark_transmit_window.rs | 4 +- tests/bookmark_when.rs | 30 +-- tests/bootstrap.rs | 11 +- tests/causal.rs | 142 ++++++++------ tests/changes.rs | 10 +- tests/common/action.rs | 29 +-- tests/common/oracle.rs | 27 ++- tests/common/overlap.rs | 41 ++-- tests/common/peer.rs | 22 +-- tests/common/schedule/arb.rs | 20 +- tests/common/schedule/events.rs | 8 +- tests/common/schedule/executor.rs | 40 ++-- tests/common/sim.rs | 151 +++++++------- tests/disruption.rs | 36 ++-- tests/gossip_snapshot.rs | 154 ++++++++------- tests/gossip_when.rs | 10 +- tests/handshake_liveness.rs | 4 +- tests/hop_trace.rs | 37 ++-- tests/lifecycle.rs | 2 +- tests/listen.rs | 148 ++++++++------ tests/membership.rs | 20 +- tests/multi_peer.rs | 40 ++-- tests/opening_supply.rs | 40 ++-- tests/pairwise.rs | 16 +- tests/partition.rs | 9 +- tests/redaction.rs | 39 ++-- tests/retire.rs | 19 +- tests/retire_redaction.rs | 10 +- tests/session_overlap.rs | 26 +-- tests/session_stats.rs | 8 +- tests/shadow_validity.rs | 39 ++-- tests/single_peer.rs | 43 ++-- tests/stale_floor.rs | 4 +- tests/target_message_size.rs | 4 +- tests/window_corners.rs | 2 +- 63 files changed, 1198 insertions(+), 1193 deletions(-) delete mode 100644 src/tree/key.rs diff --git a/AGENTS.md b/AGENTS.md index 6d138933b..59caae4f1 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 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..e0c6a43f7 100644 --- a/benches/in_memory.rs +++ b/benches/in_memory.rs @@ -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..f3c6dada1 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; @@ -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/examples/swarm.rs b/examples/swarm.rs index 4cc202076..b0abb3c5f 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. @@ -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/src/batch.rs b/src/batch.rs index 5fe06cd37..d50789320 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -3,7 +3,8 @@ 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}; /// 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 /// @@ -68,11 +69,11 @@ impl<'a, T: Send + Sync> Batch<'a, T> { 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/lib.rs b/src/lib.rs index 3cb5e37d6..45544f109 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. @@ -322,6 +322,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/peer.rs b/src/peer.rs index 81b7bb6ea..7b7f075d5 100644 --- a/src/peer.rs +++ b/src/peer.rs @@ -18,7 +18,7 @@ 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, }; @@ -549,12 +549,12 @@ impl Peer { 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/gossip/tests.rs b/src/peer/gossip/tests.rs index f2a90d2c0..a9e281c34 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(); diff --git a/src/reconciliation.rs b/src/reconciliation.rs index 6d7c1577a..d10ff905a 100644 --- a/src/reconciliation.rs +++ b/src/reconciliation.rs @@ -9,29 +9,38 @@ //! //! # 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 — is +//! detected the moment two claimants meet at one replica, and the +//! detecting operation halts: producing such a pair 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 +69,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 +106,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 +140,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 +152,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..0dcb191aa 100644 --- a/src/rumors.rs +++ b/src/rumors.rs @@ -8,7 +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 crate::{Batch, Error, Gossiped, Network, Peer, Snapshot, Version}; use borsh::{BorshDeserialize, BorshSerialize}; use futures::Stream; use std::sync::Arc; @@ -139,13 +139,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 /// @@ -167,12 +166,12 @@ impl Rumors { 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 +185,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 diff --git a/src/rumors/causal.rs b/src/rumors/causal.rs index bcc1be38e..97c674d79 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,18 @@ 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 +81,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 +94,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 +109,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 +189,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 +213,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 +229,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/tree.rs b/src/tree.rs index 955cdb0b2..a39ac70c0 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -62,13 +62,11 @@ use std::sync::Arc; -mod key; pub(crate) mod traverse; pub(crate) mod typed; use crate::{Version, causally, message::Message, tree::typed::Node}; -pub use key::Key; pub use typed::hash::MERKLE_HASH_LEN; pub mod mirror; @@ -163,23 +161,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) { @@ -189,7 +187,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())) } } @@ -273,12 +271,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())) } @@ -301,7 +301,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, @@ -343,7 +343,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, { @@ -351,7 +351,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 @@ -360,11 +360,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). /// @@ -418,8 +418,8 @@ impl Tree { 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" @@ -438,17 +438,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).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) })) } @@ -460,9 +457,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 @@ -477,7 +474,7 @@ impl Tree { 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 @@ -489,13 +486,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(); diff --git a/src/tree/arb.rs b/src/tree/arb.rs index 56a8eba32..d7e029ff5 100644 --- a/src/tree/arb.rs +++ b/src/tree/arb.rs @@ -153,7 +153,7 @@ pub fn arb_divergent_pair() -> BoxedStrategy<(crate::tree::Root<()>, crate::tree (0..n_shared).map(|_| Action::Insert(Message::new(()))), ) .expect("collision-free by construction"); - 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(); @@ -214,7 +214,7 @@ pub fn arb_wide_divergent_pair() -> BoxedStrategy<(crate::tree::Root<()>, crate: (0..n_shared).map(|_| Action::Insert(Message::new(()))), ) .expect("collision-free by construction"); - 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(); @@ -403,7 +403,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(); 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/streaming/remote/proxy/tests/greeting.rs b/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs index 33d0bdce2..7ce6f0484 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs @@ -127,9 +127,12 @@ fn empty_carried_listing_asks_for_everything() { emptied .act(&nth_party(1), [Action::Insert(Message::new(()))]) .expect("collision-free by construction"); - let keys: Vec<_> = emptied.iter().map(|(key, _, _)| key).collect(); + let paths: Vec<_> = emptied + .iter() + .map(|(v, _)| crate::tree::typed::Path::for_leaf(v)) + .collect(); emptied - .act(&nth_party(1), keys.into_iter().map(Action::Forget)) + .act(&nth_party(1), paths.into_iter().map(Action::Forget)) .expect("collision-free by construction"); assert!(emptied.is_empty(), "the initiator's tree must be empty"); @@ -251,14 +254,17 @@ fn overlapping_sessions_lose_innocent_leaf_after_honored_redaction() { let mut t0 = Tree::new(); t0.act(&p, (0..25).map(|_| Action::Insert(Message::new(())))) .expect("collision-free by construction"); - 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). @@ -290,10 +296,10 @@ fn overlapping_sessions_lose_innocent_leaf_after_honored_redaction() { .expect("collision-free by construction"); 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/tests.rs b/src/tree/tests.rs index 719c5a014..82362660e 100644 --- a/src/tree/tests.rs +++ b/src/tree/tests.rs @@ -7,13 +7,9 @@ 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() - } +/// 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,14 +78,14 @@ fn version_for(party: impl AsRef<[u8]>, ticks: u64) -> Version { v } -/// Compute the leaf-path `Key` that `Tree::act` assigns for an insert at +/// 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 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) -> Key { - Path::for_leaf(&version_for(party, scalar)).into() +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: @@ -103,7 +99,7 @@ fn insert_at( party: impl AsRef<[u8]>, scalar: u64, value: Bytes, -) -> (Key, Version, Message) { +) -> (Path, Version, Message) { (leaf_path(party, scalar), version, msg(value)) } @@ -259,7 +255,7 @@ 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()); @@ -281,7 +277,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)) @@ -317,9 +313,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()), @@ -457,7 +453,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); } @@ -480,12 +476,14 @@ proptest! { bytes.iter().cloned().map(insert_action)).expect("collision-free by construction"); } - // 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); @@ -495,8 +493,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(); @@ -548,10 +547,10 @@ 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 = (1..=bytes.len() as u64) + let present: BTreeSet = (1..=bytes.len() as u64) .map(|scalar| leaf_path(&party, scalar)) .collect(); prop_assume!(!present.contains(&nuke)); @@ -743,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(); @@ -753,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(); @@ -837,26 +836,29 @@ proptest! { 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]))]) .expect("collision-free by construction"); 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! { @@ -891,8 +893,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())]; @@ -905,21 +907,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(()) @@ -952,8 +956,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), @@ -969,23 +973,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"); } } } @@ -1030,18 +1032,26 @@ 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)]).expect("collision-free by construction"); + .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))]) + .expect("collision-free by construction"); prop_assert_eq!(tree.max_version_bytes(), naive_max_version_bytes(&tree)); } } @@ -1082,19 +1092,21 @@ 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)]).expect("collision-free by construction"); + left.act(&party_of("A"), [Action::Forget(Path::for_leaf(&version))]) + .expect("collision-free by construction"); } left.join(right).expect("collision-free by construction"); @@ -1123,14 +1135,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), ).expect("collision-free by construction"); - 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(); @@ -1139,7 +1151,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)); @@ -1259,13 +1271,13 @@ fn ceiling_only_join_reports_unchanged() { other .act(&party_of("B"), [insert_action(Bytes::from_static(b"gone"))]) .expect("collision-free by construction"); - 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)]) + .act(&party_of("B"), [Action::Forget(Path::for_leaf(&version))]) .expect("collision-free by construction"); assert!( other.is_empty(), @@ -1302,9 +1314,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!( @@ -1327,7 +1338,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", ); } @@ -1345,16 +1356,18 @@ 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 }) .expect("collision-free by construction"); - 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", @@ -1365,7 +1378,7 @@ fn escaped_version_defeats_redaction_in_a_poisoned_store() { tree.act(&receiver_party, [Action::Forget(key)]) .expect("collision-free by construction"); assert!( - tree.get(&key).is_some(), + tree.get(&escaped).is_some(), "redacting the escaped leaf is silently skipped", ); @@ -1375,7 +1388,7 @@ fn escaped_version_defeats_redaction_in_a_poisoned_store() { let mut fresh: Tree<()> = Tree::new(); fresh.join(tree).expect("collision-free by construction"); assert!( - fresh.get(&key).is_some(), + fresh.get(&escaped).is_some(), "the escaped leaf re-plants into a fresh replica", ); } @@ -1706,7 +1719,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)).into(); + let key = Path::for_leaf(&version_for("A", 2)); tree.react([(key, version_for("A", 2), existing)]) .expect("collision-free by construction"); @@ -1768,7 +1781,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)).into(); + let bomb_key = Path::for_leaf(&version_for("A", 2)); ours.act(&party_of("A"), [Action::Insert(bomb)]) .expect("collision-free by construction"); @@ -1818,7 +1831,7 @@ fn join_destructor_unwind_leaves_tree_byte_identical() { /// it as equal. #[test] fn join_detects_a_version_collision_at_one_path() { - let shared: Key = Key::from([0x42; 32]); + 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")))]) @@ -1855,7 +1868,7 @@ fn join_detects_a_version_collision_at_one_path() { #[test] fn join_prunes_same_version_payload_divergence_as_equal() { let version = version_for("A", 1); - let path: Key = Path::for_leaf(&version).into(); + let path = Path::for_leaf(&version); let mut ours: Tree = Tree::new(); ours.react([(path, version.clone(), msg(Bytes::from_static(b"ours")))]) @@ -1873,7 +1886,7 @@ fn join_prunes_same_version_payload_divergence_as_equal() { assert_eq!(ours.hash(), hash_before); let (_, message) = ours .iter() - .map(|(_, v, m)| (v.clone(), m.clone())) + .map(|(v, m)| (v.clone(), m.clone())) .next() .expect("one live message"); assert_eq!(&*message, &Bytes::from_static(b"ours"), "ours is kept"); @@ -1885,7 +1898,7 @@ fn join_prunes_same_version_payload_divergence_as_equal() { #[test] fn reinserting_an_identical_leaf_is_idempotent() { let version = version_for("A", 1); - let path: Key = Path::for_leaf(&version).into(); + let path = Path::for_leaf(&version); let message = msg(Bytes::from_static(b"same")); let mut tree: Tree = Tree::new(); @@ -1903,7 +1916,7 @@ fn reinserting_an_identical_leaf_is_idempotent() { #[test] fn react_detects_version_reuse_at_an_occupied_path() { let version = version_for("A", 1); - let path: Key = Path::for_leaf(&version).into(); + let path = Path::for_leaf(&version); let mut tree: Tree = Tree::new(); tree.react([(path, version.clone(), msg(Bytes::from_static(b"first")))]) @@ -1921,7 +1934,7 @@ fn react_detects_version_reuse_at_an_occupied_path() { /// identity check are enforced, not just payload equality. #[test] fn react_detects_a_path_collision_between_distinct_versions() { - let shared: Key = Key::from([0x24; 32]); + 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")))]) diff --git a/src/tree/typed/hash.rs b/src/tree/typed/hash.rs index 84eb58336..c04c57865 100644 --- a/src/tree/typed/hash.rs +++ b/src/tree/typed/hash.rs @@ -7,7 +7,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; /// /// 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; @@ -92,22 +92,19 @@ 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). `version` is the leaf's version in its canonical encoding, - /// which is self-delimiting, so the preimage stays injective with the - /// suffix length-tagged and the version last. A leaf commits its path - /// bytes and its version — never its message bytes: every digest the - /// mirror compares is a pure function of the version set, so no author - /// of message *content* contributes a single bit to any compared - /// quantity. The path is itself the full-width hash of the version - /// (leaves are version-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; committing the version bytes here as well - /// makes two leaves whose *distinct* versions collided into one path - /// (a full-width hash collision, off-model) digest-unequal, so the - /// merge walk surfaces that impossibility as a local violation instead - /// of silently keeping one side. + /// `suffix_len` is one byte (a compressed span never exceeds the + /// 32-byte path). `version` is the leaf's canonical encoding: + /// self-delimiting, so the preimage stays injective with the suffix + /// length-tagged and the version last. + /// + /// A leaf commits its path bytes and its version, never its message + /// bytes: every compared digest is a pure function of the version set, + /// and a content author contributes no bit to any compared quantity. + /// The path already commits the version through its hash + /// ([`Path::for_leaf`](super::Path::for_leaf)); committing the raw + /// version bytes too makes two *distinct* versions that collided into + /// one path (off-model) digest-unequal, so the merge walk surfaces + /// that impossibility as a local violation instead of keeping a side. /// /// # Panics /// diff --git a/src/tree/typed/node.rs b/src/tree/typed/node.rs index a1725f441..9ed290e6e 100644 --- a/src/tree/typed/node.rs +++ b/src/tree/typed/node.rs @@ -382,11 +382,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) } diff --git a/src/tree/typed/path.rs b/src/tree/typed/path.rs index 143842c60..705d11bb6 100644 --- a/src/tree/typed/path.rs +++ b/src/tree/typed/path.rs @@ -18,26 +18,20 @@ pub struct Path { } impl Path { - /// Get a path for a leaf stamped with `version`: the full-width hash of - /// the version's canonical bytes, and nothing else. + /// Get the path for a leaf stamped with `version`: the full-width hash + /// of the version's canonical bytes, and nothing else. /// - /// The version alone determines where a leaf lives. Its canonical - /// [`as_bytes`](Version::as_bytes) is unique per insert: every - /// [`tick`](Version::tick) yields a distinct canonical encoding (local - /// uniqueness), and parties descend from a shared seed by disjoint - /// forks, so no two parties ever stamp the same version (global - /// uniqueness by party disjointness). Those are invariants the - /// protocol already rests on everywhere, so deriving identity from the - /// version adds no new assumption — and it keeps message bytes out of - /// every path and digest, so no actor can steer where anything lands by - /// choosing content. + /// 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 `ContentHash`, never the /// truncated Merkle `Hash`: a path collision is permanent split-brain - /// (see `ContentHash`), so identity gets the full width even though the - /// comparison digests are narrower. The preimage is one canonical byte - /// string (self-delimiting, no second component), so no - /// length-extension or concatenation ambiguity arises. + /// (see `ContentHash`). The preimage is one self-delimiting canonical + /// byte string, so no concatenation ambiguity arises. pub fn for_leaf(version: &Version) -> Self { Self { height: PhantomData, diff --git a/src/tree/typed/prefix.rs b/src/tree/typed/prefix.rs index 36fe74c9a..afa5d07ad 100644 --- a/src/tree/typed/prefix.rs +++ b/src/tree/typed/prefix.rs @@ -3,8 +3,6 @@ use std::{fmt::Debug, marker::PhantomData}; use borsh::{BorshDeserialize, BorshSerialize}; use tinyvec::ArrayVec; -use crate::tree::Key; - use super::height::{Height, Root, S, Z}; use super::path::Path; @@ -39,17 +37,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 +78,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 } 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/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..9f2dc9c22 100644 --- a/tests/async_wire.rs +++ b/tests/async_wire.rs @@ -6,9 +6,9 @@ //! //! 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`) diff --git a/tests/bookmark_causality.rs b/tests/bookmark_causality.rs index f36191e2c..063263e48 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}; @@ -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..57151f410 100644 --- a/tests/bootstrap.rs +++ b/tests/bootstrap.rs @@ -50,8 +50,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,7 +82,7 @@ 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", ); } @@ -111,7 +112,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 +190,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/causal.rs b/tests/causal.rs index 59c0435ab..11ca2a8fb 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), @@ -551,11 +573,11 @@ proptest! { borsh::to_vec(causal.checkpoint()).expect("a checkpoint serializes"); 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:?}"), } @@ -563,17 +585,17 @@ proptest! { let unordered_checkpoint = borsh::to_vec(unordered.checkpoint()).expect("a checkpoint serializes"); - 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. @@ -594,7 +616,9 @@ proptest! { 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", ); } @@ -606,7 +630,9 @@ proptest! { 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/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..3e479d304 100644 --- a/tests/common/action.rs +++ b/tests/common/action.rs @@ -3,7 +3,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use proptest::collection::vec; use proptest::prelude::*; -use rumors::{Key, Snapshot, Version, causally}; +use rumors::{Snapshot, Version, causally}; const MAX_ACTIONS: usize = 16; @@ -15,7 +15,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,23 +43,23 @@ 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. @@ -66,17 +67,17 @@ pub fn build_local(local: rumors::Rumors, actions: &[LocalAction]) -> r where T: Send + Sync + Clone + BorshSerialize + BorshDeserialize + '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/oracle.rs b/tests/common/oracle.rs index ff11953fe..474428453 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,28 @@ 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..d73b76a43 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; @@ -35,7 +35,7 @@ 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}; @@ -164,9 +164,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, @@ -212,7 +212,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 +220,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 +522,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 +607,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..f6705a932 100644 --- a/tests/common/peer.rs +++ b/tests/common/peer.rs @@ -14,7 +14,7 @@ //! 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}; @@ -32,7 +32,7 @@ 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 { @@ -55,7 +55,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 +64,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(); 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..118b9aca0 100644 --- a/tests/common/schedule/events.rs +++ b/tests/common/schedule/events.rs @@ -11,10 +11,10 @@ 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..85e81dcc7 100644 --- a/tests/common/schedule/executor.rs +++ b/tests/common/schedule/executor.rs @@ -12,7 +12,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; @@ -23,13 +23,14 @@ use crate::common::wire::{LINK_BUF, assert_control_drained, block_on, bootstrap_ 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 +38,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 { @@ -93,8 +95,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 /// @@ -128,7 +130,7 @@ where ExecutionResult { peers, oracle: result.oracle, - resolved_keys: result.resolved_keys, + resolved_versions: result.resolved_versions, } } @@ -190,31 +192,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 +281,6 @@ where slots, retired_observations, oracle, - resolved_keys, + resolved_versions, } } 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/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..586f978db 100644 --- a/tests/gossip_snapshot.rs +++ b/tests/gossip_snapshot.rs @@ -20,7 +20,7 @@ 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")] @@ -38,17 +38,24 @@ 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}")) } +/// A leaf's tree path: the full-width BLAKE3 hash of its version's +/// canonical bytes. The fixture self-checks read path bytes through this +/// to verify the tree shapes the pinned sessions rely on. +fn leaf_path(version: &Version) -> [u8; 32] { + *blake3::hash(version.as_bytes()).as_bytes() +} + /// Two empty peers: the minimal session. /// /// After the 25-byte preamble @@ -83,24 +90,23 @@ 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). +/// The two payload values batch-sent into the seeded universe of +/// [`batched_supply_run`]. The fixture requires the two minted leaves' +/// paths (each the hash of its version) to share their first two bytes: +/// 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). +/// The self-check below enforces the shape. const COLLIDING_VALUES: (u64, u64) = (1, 27730); /// 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 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. +/// 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) = block_on(async { @@ -115,12 +121,15 @@ fn batched_supply_run() { 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.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" ); insta::assert_snapshot!(capture_gossip(a, b)); } @@ -178,22 +187,21 @@ 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. +/// The two payload values batch-sent into the seeded universe of +/// [`bulk_initiator_ships_opening_supplies`]. The fixture requires the two +/// minted leaves' paths to share their first byte with distinct second +/// bytes, so the initiator's one exclusive root child holds a two-leaf +/// subtree whose leaves split one level down. The self-checks below +/// enforce the shape. const INITIATOR_SUBTREE_VALUES: (u64, u64) = (1, 287); /// 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. +/// The fixture requires their leaf paths' first bytes to avoid the +/// initiator's exclusive radix, and the extra message makes the responder +/// the larger set, so the subtree holder wins the initiator election. The +/// self-checks below enforce the shape. const RESPONDER_BALLAST_FROM: u64 = 100; /// A bulk-holding initiator ships its exclusive root children whole at the @@ -201,7 +209,7 @@ const RESPONDER_BALLAST_FROM: u64 = 100; /// 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 @@ -219,26 +227,27 @@ fn bulk_initiator_ships_opening_supplies() { }); // 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!( @@ -294,7 +303,7 @@ fn early_supplies_honor_redactions() { a.send(1); let b = bootstrap_fork_async(&a).await; a.send(REDACTION_SUBTREE_VALUE); - b.redact(key_for(&b, 1)); + b.redact(&version_for(&b, 1)); let y = REDACTION_BALLAST_FROM; b.batch().send(y).send(y + 1).send(y + 2); (a, b) @@ -302,22 +311,16 @@ fn early_supplies_honor_redactions() { // 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 +338,23 @@ 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), + .any(|(_, m)| **m == REDACTION_SUBTREE_VALUE), "the survivor converges to the initiator" ); assert!( b.snapshot() .iter() - .any(|(_, _, m)| **m == REDACTION_SUBTREE_VALUE), + .any(|(_, m)| **m == REDACTION_SUBTREE_VALUE), "the survivor converges to the responder" ); insta::assert_snapshot!(capture); @@ -397,7 +400,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 +408,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 +447,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)); @@ -526,7 +529,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,10 +538,11 @@ 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() { @@ -546,9 +550,9 @@ fn both_redact_same_key() { 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..1f41fb443 100644 --- a/tests/hop_trace.rs +++ b/tests/hop_trace.rs @@ -40,7 +40,7 @@ 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; @@ -461,8 +461,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 +476,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,10 +528,12 @@ 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) { let left = Peer::seed_rng(&mut SmallRng::seed_from_u64(0)) .sync_memory_budget(DEFAULT_SYNC_MEMORY_BUDGET) @@ -540,19 +542,18 @@ fn transfer_pair() -> (Rumors, Rumors) { 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 - .snapshot() - .iter() - .map(|(k, _, _)| k.as_bytes()[0]) - .collect(); + // Fixture self-checks: mirror the required shape so drift in hashing + // or version assignment fails here, not in the hop arithmetic. 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 radices: Vec = left.snapshot().iter().map(|(v, _)| path_radix(v)).collect(); 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..16b3b2d44 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, ); } diff --git a/tests/opening_supply.rs b/tests/opening_supply.rs index ec5a1beab..d31b541c3 100644 --- a/tests/opening_supply.rs +++ b/tests/opening_supply.rs @@ -14,7 +14,7 @@ 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::wire::{block_on, bootstrap_fork_async}; @@ -24,17 +24,19 @@ 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. +/// A second message for the staging: the fixture requires its leaf path +/// (the hash of its version) to share its first byte with message `1`'s, +/// so the two sides dispute one root child. /// /// The initiator holds both leaves, the -/// responder — forked between the two sends — only the first. +/// responder — forked between the two sends — only the first. The +/// self-checks below enforce the shape. const DISPUTED_SIBLING_VALUE: u64 = 165; -/// 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. +/// First of three consecutive responder ballast values. The fixture +/// requires their leaf paths' first bytes to avoid the disputed radix and +/// makes the responder the larger set, so the disputed-subtree holder +/// initiates. The self-checks below enforce the shape. const BALLAST_FROM: u64 = 100; /// Count the frames whose semantic label starts with `label` in a rendered @@ -73,19 +75,21 @@ fn divergent_root_child_has_one_question_owner() { }); // 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..9f70a84a5 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 @@ -192,7 +192,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 +200,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 +212,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..f3e744f36 100644 --- a/tests/session_stats.rs +++ b/tests/session_stats.rs @@ -95,13 +95,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..a221b4d60 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,13 @@ 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)> { +/// 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 +27,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 +42,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 +86,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 +135,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 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/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. From f2b74a9729d5bfaafbf0f7d1bf690a93f0efd91a Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 20:18:02 -0400 Subject: [PATCH 03/11] wire: retire borsh for CBOR payloads and framed canonical atoms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version-only addressing frees the payload encoding from canonicity, so T's bounds become serde's (Serialize + DeserializeOwned) and the cached payload is one CBOR value via ciborium — self-describing, so field and variant names are the wire contract (reordering-compatible, pinned end to end by tests/cbor_evolution.rs), with unknown fields skipped and missing fields erroring absent a serde default. Message::new keeps its documented panic: serializability is a stated caller obligation, and with CBOR imposing no format-driven failures the only trigger left is T's own Serialize declining a value. before's types ride their existing canonical codec everywhere, framed rather than re-encoded: as bare bytes where a frame already delimits them (the greeting version, the party hand-off) and as single CBOR byte-string values where the stream must delimit itself (leaf records, the V1 node bodies, the bookmark payload's clocks via before/serde). The V2 leaf record is now record-header ‖ CBOR(version) ‖ payload; record_len prices that framing exactly, pinned against an actual push. The greeting listing drops its count prefix for the codec's raw radix-hash record shape. The V1 alternating messages keep their structural framing over a crate-internal wire codec (tree::wire) whose method names deliberately avoid before's inherent encode_to — an inherent method silently shadows a trait method, which cost one misaligned wire during this migration. The bookmark payload becomes CBOR and its on-disk format version bumps to 3; older versions are rejected loudly, as before. Wire-format snapshot pins (insta, bookmark frames, codec atlas) are deliberately left red for the named re-acceptance commit that follows. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- Cargo.lock | 68 +---- Cargo.toml | 9 +- benches/support/latency.rs | 9 +- benches/support/wire.rs | 5 +- proptest-regressions/message/tests.txt | 7 + .../tree/mirror/alternating/message/tests.txt | 9 + .../tree/mirror/alternating/tests.txt | 11 + src/batch.rs | 3 +- src/bookmark/format.rs | 44 ++- src/bookmark/format/tests.rs | 15 +- src/error.rs | 2 +- src/lib.rs | 1 - src/message.rs | 158 +++++++---- src/message/tests.rs | 107 ++++--- src/network.rs | 24 +- src/peer.rs | 5 +- src/peer/bootstrap.rs | 5 +- src/peer/gossip.rs | 13 +- src/rumors.rs | 7 +- src/tests.rs | 13 +- src/tree.rs | 1 + src/tree/mirror/alternating/backend/remote.rs | 45 ++- .../alternating/backend/remote/tests.rs | 22 +- src/tree/mirror/alternating/message.rs | 147 +++++----- src/tree/mirror/alternating/message/tests.rs | 64 ++--- src/tree/mirror/alternating/tests.rs | 3 +- src/tree/mirror/alternating/wire_snapshot.rs | 11 +- src/tree/mirror/party.rs | 20 +- src/tree/mirror/party/tests.rs | 18 +- .../mirror/streaming/remote/adapter/decode.rs | 12 +- .../remote/adapter/tests/malformed.rs | 2 +- .../mirror/streaming/remote/codec/capture.rs | 33 ++- .../streaming/remote/codec/capture/tests.rs | 21 +- .../mirror/streaming/remote/codec/decode.rs | 15 +- .../streaming/remote/codec/decode/async_io.rs | 8 +- .../streaming/remote/codec/decode/tests.rs | 39 +-- .../mirror/streaming/remote/codec/encode.rs | 2 +- .../streaming/remote/codec/encode/tests.rs | 19 +- .../mirror/streaming/remote/codec/error.rs | 12 +- .../mirror/streaming/remote/codec/frame.rs | 88 +++++- .../streaming/remote/codec/frame/tests.rs | 47 +++ .../mirror/streaming/remote/codec/tests.rs | 2 +- .../remote/codec/tests/error_atlas.rs | 11 +- .../mirror/streaming/remote/proxy/start.rs | 43 ++- .../streaming/remote/proxy/start/tests.rs | 30 +- .../mirror/streaming/remote/proxy/state.rs | 18 +- .../mirror/streaming/remote/proxy/tests.rs | 6 +- .../mirror/streaming/remote/proxy/work.rs | 2 +- .../streaming/remote/proxy/work/pump.rs | 6 +- src/tree/mirror/streaming/remote/streams.rs | 7 +- src/tree/mirror/streaming/tests/fixtures.rs | 5 +- src/tree/tests.rs | 4 +- src/tree/typed/hash.rs | 10 +- src/tree/typed/node.rs | 84 +++--- src/tree/typed/prefix.rs | 11 +- src/tree/typed/tests.rs | 28 +- src/tree/typed/untyped.rs | 36 +-- src/tree/wire.rs | 268 ++++++++++++++++++ tests/bootstrap.rs | 2 +- tests/bootstrap_snapshot.rs | 3 +- tests/causal.rs | 14 +- tests/cbor_evolution.rs | 176 ++++++++++++ tests/common/action.rs | 3 +- tests/common/flaky.rs | 6 +- tests/common/gossip_snapshot.rs | 5 +- tests/common/overlap.rs | 5 +- tests/common/peer.rs | 11 +- tests/common/schedule/executor.rs | 13 +- tests/common/wire.rs | 19 +- tests/dispute_wire.rs | 7 +- tests/hop_trace.rs | 2 +- tests/pairwise.rs | 3 +- tests/session_stats.rs | 2 +- tests/single_peer.rs | 8 +- tests/tradeoff_probe.rs | 7 +- 75 files changed, 1327 insertions(+), 674 deletions(-) create mode 100644 proptest-regressions/message/tests.txt create mode 100644 proptest-regressions/tree/mirror/alternating/message/tests.txt create mode 100644 proptest-regressions/tree/mirror/alternating/tests.txt create mode 100644 src/tree/wire.rs create mode 100644 tests/cbor_evolution.rs 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/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/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 d50789320..e2b4b8322 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -1,4 +1,3 @@ -use borsh::BorshSerialize; use tokio::sync::watch; use crate::message::Message; @@ -63,7 +62,7 @@ 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: serde::Serialize, { self.actions.push(Action::Insert(Message::from(message))); self diff --git a/src/bookmark/format.rs b/src/bookmark/format.rs index 0db1f749d..ec396803a 100644 --- a/src/bookmark/format.rs +++ b/src/bookmark/format.rs @@ -8,13 +8,13 @@ //! [ 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 @@ -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/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/error.rs b/src/error.rs index db522c562..7145ddcd6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -55,7 +55,7 @@ pub enum Error { /// An underlying reader/writer error, or a Borsh 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 45544f109..e005450e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -304,7 +304,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::{ diff --git a/src/message.rs b/src/message.rs index 79b502793..c13538bf0 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,21 +1,35 @@ 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; /// 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 +44,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: serde::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: serde::de::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: serde::de::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 +150,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: serde::Serialize, { Message { - serialized: Bytes::from(borsh::to_vec(&*arc).unwrap()), + serialized: Bytes::from(to_vec(&*arc)), message: arc, } } @@ -139,13 +212,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 +273,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 serde::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: serde::de::DeserializeOwned> serde::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..218ca96f4 100644 --- a/src/message/tests.rs +++ b/src/message/tests.rs @@ -1,16 +1,16 @@ 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)] +/// 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 +31,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 +53,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 +63,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 serde::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..c96f49986 100644 --- a/src/network.rs +++ b/src/network.rs @@ -2,7 +2,6 @@ use std::fmt; -use borsh::{BorshDeserialize, BorshSerialize}; use rand::RngCore; /// The identifier shared by every [`Rumors`](crate::Rumors) that descends from @@ -18,9 +17,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 serde::Serialize for Network { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_bytes(&self.0) + } +} + +impl<'de> serde::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 7b7f075d5..a0a7a9d7d 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}; @@ -274,7 +273,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: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -542,7 +541,7 @@ impl Peer { pub(crate) fn send(&self, message: T) -> Batch<'_, T> where - T: BorshSerialize + Send + Sync, + T: serde::Serialize + Send + Sync, { let mut batch = self.batch(); batch.send(message); diff --git a/src/peer/bootstrap.rs b/src/peer/bootstrap.rs index 7d00d55cf..120a5e2cc 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}; @@ -216,7 +215,7 @@ impl Bootstrap { link: &mut Link, ) -> Result>, Error> where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -296,7 +295,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: serde::de::DeserializeOwned + serde::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 6e62a52c9..68aae1024 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::{ @@ -210,7 +209,7 @@ impl Peer { link: &'a mut Link, ) -> BoxFuture<'a, Result, Error>> where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -237,7 +236,7 @@ impl Peer { link: DynLinkParts<'a>, ) -> BoxFuture<'a, Result, Error>> where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, { Box::pin(async move { let (read, write, connector, acceptor, epoch) = link; @@ -436,7 +435,7 @@ impl Peer { link: &mut Link, ) -> Retire where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -475,7 +474,7 @@ impl Peer { link: &mut Link, ) -> Result> where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -615,7 +614,7 @@ impl Peer { link: DynLinkParts<'a>, ) -> (Intent, Result<(Version, SessionStats), Error>) where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, { let (read, write, connector, acceptor, epoch) = link; // The session's stats recorder: under V2, both protocol @@ -970,7 +969,7 @@ impl Peer { link: &'a mut Link, ) -> impl Stream>> + Unpin + 'a where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, diff --git a/src/rumors.rs b/src/rumors.rs index 0dcb191aa..39df14e91 100644 --- a/src/rumors.rs +++ b/src/rumors.rs @@ -9,7 +9,6 @@ pub use unordered::{TryNext, UnorderedMessages}; use crate::bookmark::{Bookmark, BookmarkError, NoBookmark}; use crate::link::{Acceptor, Connector, Link}; use crate::{Batch, Error, Gossiped, Network, Peer, Snapshot, Version}; -use borsh::{BorshDeserialize, BorshSerialize}; use futures::Stream; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -161,7 +160,7 @@ impl Rumors { /// If `message` fails to serialize (see [`Batch::send`]). pub fn send(&self, message: T) -> Batch<'_, T> where - T: BorshSerialize + Send + Sync, + T: serde::Serialize + Send + Sync, { self.peer.send(message) } @@ -392,7 +391,7 @@ impl Rumors { link: &mut Link, ) -> Result> where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -508,7 +507,7 @@ impl Rumors { link: &'a mut Link, ) -> impl Stream>> + Unpin + 'a where - T: BorshDeserialize + BorshSerialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, diff --git a/src/tests.rs b/src/tests.rs index 6f320d989..1a2fe3f84 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 a39ac70c0..36ddbb6f7 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -64,6 +64,7 @@ use std::sync::Arc; pub(crate) mod traverse; pub(crate) mod typed; +pub(crate) mod wire; use crate::{Version, causally, message::Message, tree::typed::Node}; diff --git a/src/tree/mirror/alternating/backend/remote.rs b/src/tree/mirror/alternating/backend/remote.rs index 40c70fb69..8fb5b9eb8 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}; @@ -123,7 +123,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 +170,7 @@ impl protocol::Accept for Exchange where R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, - T: BorshSerialize + BorshDeserialize + Send + Sync, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync, { type Next = Exchange; @@ -209,10 +208,10 @@ where impl protocol::Initiator for Exchange where - T: BorshDeserialize + Send + Sync, + T: serde::de::DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, - Node: BorshDeserialize, + Node: wire::Decode, { type Next = Exchange; @@ -231,10 +230,10 @@ where impl protocol::Responder for Exchange where - T: BorshDeserialize + Send + Sync, + T: serde::de::DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, - Node: BorshDeserialize, + Node: wire::Decode, { type Next = Exchange; @@ -259,10 +258,10 @@ where impl protocol::OpenInitiator for Exchange where - T: BorshDeserialize + Send + Sync, + T: serde::de::DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, - Node: BorshDeserialize, + Node: wire::Decode, { type Next = Exchange; @@ -293,13 +292,13 @@ where impl protocol::Exchange for Exchange>> where - T: BorshDeserialize + Send + Sync, + T: serde::de::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 +341,7 @@ where impl protocol::CloseResponder for Exchange> where - T: BorshDeserialize + Send + Sync, + T: serde::de::DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, { @@ -383,7 +382,7 @@ where impl protocol::CompleteInitiator for Exchange where - T: BorshDeserialize + Send + Sync, + T: serde::de::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..d8c593c2c 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`](crate::tree::wire) +//! codec: explicit structural framing whose variable-width atoms are +//! single CBOR values. Container lengths are `u32` little-endian. //! //! ## Atoms //! @@ -12,14 +12,14 @@ //! 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) //! @@ -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::{ @@ -99,16 +99,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 +120,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 +151,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 +206,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: serde::de::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 +300,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: serde::de::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 +347,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: serde::de::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 +375,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 +384,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 +393,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..25aac4da8 100644 --- a/src/tree/mirror/alternating/message/tests.rs +++ b/src/tree/mirror/alternating/message/tests.rs @@ -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/tests.rs b/src/tree/mirror/alternating/tests.rs index 43169eb54..a28e1d37e 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; @@ -83,7 +82,7 @@ fn mirror_via( scenario: Scenario, ) -> crate::tree::Root where - T: PartialEq + std::fmt::Debug + BorshSerialize + BorshDeserialize + Send + Sync, + T: PartialEq + std::fmt::Debug + serde::Serialize + serde::de::DeserializeOwned + Send + Sync, { block_on(async move { match scenario { 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/remote/adapter/decode.rs b/src/tree/mirror/streaming/remote/adapter/decode.rs index ad92b7856..f1e93ca34 100644 --- a/src/tree/mirror/streaming/remote/adapter/decode.rs +++ b/src/tree/mirror/streaming/remote/adapter/decode.rs @@ -85,7 +85,7 @@ pub fn early_supplies( ) -> impl Stream), DecodeError>> + Send where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, G: Convert, S: Height, F: Stream> + Unpin + Send + 'static, @@ -150,7 +150,7 @@ async fn read_early( ) -> Result<(), DecodeError> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, G: Height, S: Height, F: Stream> + Unpin, @@ -219,7 +219,7 @@ pub async fn decode_reply( ) -> Result, Vec>>, DecodeError> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, H: Height, S: Convert, S>: Height, @@ -249,7 +249,7 @@ pub async fn decode_leaf_reply( ) -> Result>>, DecodeError> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, F: Stream> + Unpin, { decode( @@ -279,7 +279,7 @@ async fn decode( ) -> Result>, DecodeError> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, H: Convert, S: Height, F: Stream> + Unpin, @@ -323,7 +323,7 @@ async fn read_reply( ) -> Result>, DecodeError> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, H: Height, S: Height, F: Stream> + Unpin, diff --git a/src/tree/mirror/streaming/remote/adapter/tests/malformed.rs b/src/tree/mirror/streaming/remote/adapter/tests/malformed.rs index bead58915..522b58c06 100644 --- a/src/tree/mirror/streaming/remote/adapter/tests/malformed.rs +++ b/src/tree/mirror/streaming/remote/adapter/tests/malformed.rs @@ -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/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..6858cce19 100644 --- a/src/tree/mirror/streaming/remote/codec/capture/tests.rs +++ b/src/tree/mirror/streaming/remote/codec/capture/tests.rs @@ -19,6 +19,17 @@ use super::super::frame::LeafRun; /// /// That one-line diff is the field-level account an insta re-accept /// shows beside the hex. + +/// 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 +} + #[test] fn supply_decode_names_the_field_that_moved() { let party = before::Party::seed(); @@ -41,9 +52,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 +94,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 +163,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..3530a447a 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}; @@ -30,7 +28,7 @@ use super::{ /// 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 +38,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 +75,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 +90,10 @@ 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..7914a5fd2 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::{ @@ -62,7 +62,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 +107,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 +193,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..ea34d9db7 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), } @@ -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..da60c1495 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, @@ -55,8 +53,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 +135,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 +159,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 +207,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: serde::de::DeserializeOwned, { self.record_slices().map(parse_record) } @@ -253,20 +262,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/tests.rs b/src/tree/mirror/streaming/remote/codec/tests.rs index 0d842b8d0..a6af68d68 100644 --- a/src/tree/mirror/streaming/remote/codec/tests.rs +++ b/src/tree/mirror/streaming/remote/codec/tests.rs @@ -66,7 +66,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..7484f4b8e 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::{ @@ -82,7 +81,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 +313,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 +529,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 +580,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/proxy/start.rs b/src/tree/mirror/streaming/remote/proxy/start.rs index 3080aba6f..9ad0282da 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,6 +29,7 @@ use crate::{ }, typed::{ Hash, + hash::MERKLE_HASH_LEN, height::{Root, Z}, }, }, @@ -120,7 +120,7 @@ where impl Connect for Handshaking where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, C: Connector, @@ -147,7 +147,7 @@ where impl CompleteConnect for Handshaking where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, C: Connector, @@ -181,7 +181,7 @@ where impl Accept for Handshaking where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, C: Connector, @@ -246,7 +246,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 +277,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 +322,7 @@ fn connected( ) -> Connected where B: Backend: Leaf>, - T: BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, C: Connector, A: Acceptor, { @@ -354,7 +377,7 @@ fn open( ) -> Connected where B: Backend: Leaf>, - T: BorshDeserialize + Send + Sync + 'static, + T: serde::de::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..b9ae5bfea 100644 --- a/src/tree/mirror/streaming/remote/proxy/start/tests.rs +++ b/src/tree/mirror/streaming/remote/proxy/start/tests.rs @@ -69,6 +69,18 @@ fn ticked_version() -> Version { /// greeting; a peer that closes mid-header must surface /// [`Error::HandshakeRead`] with `UnexpectedEof` — never a hang waiting on /// bytes that cannot arrive. + +/// 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 +} + #[pollster::test] async fn truncated_version_header_is_a_typed_read_error() { let result = receive_greeting(&[0, 0]).await.map(|_| ()); @@ -209,7 +221,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 +244,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!( @@ -256,7 +268,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 +281,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 +307,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..d550db9f7 100644 --- a/src/tree/mirror/streaming/remote/proxy/state.rs +++ b/src/tree/mirror/streaming/remote/proxy/state.rs @@ -50,7 +50,7 @@ where impl Session where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, C: Connector, A: Acceptor, { @@ -109,7 +109,7 @@ where impl Connected where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, C: Connector, A: Acceptor, { @@ -232,7 +232,7 @@ where impl protocol::CompleteEqual for Connected where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, C: Connector, @@ -250,7 +250,7 @@ where impl protocol::Initiator for Connected where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, C: Connector, @@ -282,7 +282,7 @@ where impl protocol::Responder for Connected where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, C: Connector, @@ -321,7 +321,7 @@ where impl protocol::Reply for Descending>, R, W, C, A> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, C: Connector, @@ -357,7 +357,7 @@ where impl protocol::Reply for Descending, R, W, C, A> where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, C: Connector, @@ -391,7 +391,7 @@ where impl protocol::CompleteInitiator for Completing where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, C: Connector, @@ -414,7 +414,7 @@ where impl protocol::CompleteResponder for Descending where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::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 b146904c0..d92b811d9 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests.rs @@ -72,7 +72,7 @@ async fn reconcile_symmetric_accepts( transport_capacity: usize, ) -> (TreeRoot, TreeRoot) where - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::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 +103,7 @@ async fn reconcile_symmetric_accepts_reordered( reordered: Arc, ) -> (TreeRoot, TreeRoot) where - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::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 +123,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: serde::de::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); diff --git a/src/tree/mirror/streaming/remote/proxy/work.rs b/src/tree/mirror/streaming/remote/proxy/work.rs index 31b45a182..faad5c55d 100644 --- a/src/tree/mirror/streaming/remote/proxy/work.rs +++ b/src/tree/mirror/streaming/remote/proxy/work.rs @@ -88,7 +88,7 @@ where impl Work where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::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..4590e5486 100644 --- a/src/tree/mirror/streaming/remote/proxy/work/pump.rs +++ b/src/tree/mirror/streaming/remote/proxy/work/pump.rs @@ -54,7 +54,7 @@ use super::{encode, queues}; impl Work where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, R: Send, W: Send, A: Acceptor, @@ -367,7 +367,7 @@ where impl Early where B: Backend: Leaf>, - T: borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, G: Convert, S: Height, Rx: tokio::io::AsyncRead + Unpin + Send + 'static, @@ -465,7 +465,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: serde::de::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..17e84fdaf 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}, @@ -312,7 +311,7 @@ struct ReceiverStart { impl StreamReceiver where Rx: tokio::io::AsyncRead + Unpin + Send + 'static, - T: BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, { /// Bind one incoming logical stream to its claim slot. pub fn new( @@ -383,7 +382,7 @@ pub enum ReceiverFinish { impl futures::Stream for StreamReceiver where Rx: tokio::io::AsyncRead + Unpin + Send + 'static, - T: BorshDeserialize + Send + Sync + 'static, + T: serde::de::DeserializeOwned + Send + Sync + 'static, { type Item = Frame; @@ -406,7 +405,7 @@ fn read_frames( ) -> impl futures::Stream> + Send where Rx: tokio::io::AsyncRead + Unpin + Send + 'static, - T: BorshDeserialize + Send + Sync + 'static, + T: serde::de::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 e03531bfa..507e41cec 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::{ @@ -39,7 +38,7 @@ pub(super) fn grown( paths: &[Path], ) -> Option> where - T: BorshSerialize + Clone + Send + Sync, + T: serde::Serialize + Clone + Send + Sync, { assert!(stride > 0, "each leaf needs a fresh version"); let party = nth_party(party); @@ -238,7 +237,7 @@ impl Divergence { /// in both. pub fn trees(&self, value: &T) -> (Root, Root, Root) where - T: BorshSerialize + Clone + Send + Sync, + T: serde::Serialize + Clone + Send + Sync, { let as_paths = |bytes: Vec<[u8; 32]>| -> Vec { bytes.into_iter().map(Path::from).collect() }; diff --git a/src/tree/tests.rs b/src/tree/tests.rs index 82362660e..6d7813502 100644 --- a/src/tree/tests.rs +++ b/src/tree/tests.rs @@ -302,7 +302,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); @@ -1623,7 +1623,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, serde::Serialize)] struct DropBomb { armed: bool, } diff --git a/src/tree/typed/hash.rs b/src/tree/typed/hash.rs index c04c57865..422451ca3 100644 --- a/src/tree/typed/hash.rs +++ b/src/tree/typed/hash.rs @@ -1,8 +1,6 @@ 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 @@ -13,8 +11,8 @@ 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 @@ -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]); diff --git a/src/tree/typed/node.rs b/src/tree/typed/node.rs index 9ed290e6e..32e7be999 100644 --- a/src/tree/typed/node.rs +++ b/src/tree/typed/node.rs @@ -1,16 +1,16 @@ 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; +use crate::tree::wire; use untyped::fan::{self, Fan}; /// The typed node with a height of 32; the root of the tree. @@ -433,18 +433,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`. // @@ -458,88 +459,71 @@ 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 +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 +impl wire::Decode for Node where - T: BorshDeserialize, + T: serde::de::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> +impl wire::Decode for Node> where - T: BorshDeserialize, + T: serde::de::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/prefix.rs b/src/tree/typed/prefix.rs index afa5d07ad..2d1ff51fc 100644 --- a/src/tree/typed/prefix.rs +++ b/src/tree/typed/prefix.rs @@ -1,8 +1,9 @@ use std::{fmt::Debug, marker::PhantomData}; -use borsh::{BorshDeserialize, BorshSerialize}; use tinyvec::ArrayVec; +use crate::tree::wire; + use super::height::{Height, Root, S, Z}; use super::path::Path; @@ -156,8 +157,8 @@ 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<()> { +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(), @@ -170,8 +171,8 @@ impl BorshSerialize for Prefix { } } -impl BorshDeserialize for Prefix { - fn deserialize_reader(reader: &mut R) -> borsh::io::Result { +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 081eef6b5..642756e16 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}; @@ -652,9 +651,9 @@ 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`] 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 +664,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 +674,20 @@ 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)?; + 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 +695,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)?; } } diff --git a/src/tree/wire.rs b/src/tree/wire.rs new file mode 100644 index 000000000..b59920c70 --- /dev/null +++ b/src/tree/wire.rs @@ -0,0 +1,268 @@ +//! 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; + +/// 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/tests/bootstrap.rs b/tests/bootstrap.rs index 57151f410..ccc89047c 100644 --- a/tests/bootstrap.rs +++ b/tests/bootstrap.rs @@ -30,7 +30,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { block_on(async move { let (mut a_link, mut b_link) = rumors::link::memory_with_capacity(LINK_BUF); diff --git a/tests/bootstrap_snapshot.rs b/tests/bootstrap_snapshot.rs index 78106aeb7..b0c65d995 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")] @@ -50,7 +49,7 @@ fn seeded() -> Rumors { /// expected to be served a successor. fn capture_bootstrap(provider: Rumors) -> String where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { capture_session( move |mut link| async move { diff --git a/tests/causal.rs b/tests/causal.rs index 11ca2a8fb..c7741c691 100644 --- a/tests/causal.rs +++ b/tests/causal.rs @@ -569,8 +569,7 @@ 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<(Version, u64)> = Vec::new(); @@ -582,8 +581,7 @@ proptest! { 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 .iter() @@ -608,8 +606,8 @@ 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); @@ -623,8 +621,8 @@ proptest! { ); } - 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 { diff --git a/tests/cbor_evolution.rs b/tests/cbor_evolution.rs new file mode 100644 index 000000000..617f2ad83 --- /dev/null +++ b/tests/cbor_evolution.rs @@ -0,0 +1,176 @@ +//! 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; + +/// 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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + B: serde::Serialize + serde::de::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/common/action.rs b/tests/common/action.rs index 3e479d304..61ef8dd02 100644 --- a/tests/common/action.rs +++ b/tests/common/action.rs @@ -1,6 +1,5 @@ //! Generic `Insert`/`Redact` action sequences shared by reconciliation tests. -use borsh::{BorshDeserialize, BorshSerialize}; use proptest::collection::vec; use proptest::prelude::*; use rumors::{Snapshot, Version, causally}; @@ -65,7 +64,7 @@ pub fn minted_version(snapshot: &Snapshot, pre: &Version) -> /// 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 + serde::Serialize + serde::de::DeserializeOwned + 'static, { let mut versions: Vec = Vec::new(); for a in actions { 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..6d2cd9c53 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, @@ -364,7 +363,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { capture_session( move |mut link| async move { @@ -379,7 +378,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { capture_session_v1( move |mut link| async move { diff --git a/tests/common/overlap.rs b/tests/common/overlap.rs index d73b76a43..458506113 100644 --- a/tests/common/overlap.rs +++ b/tests/common/overlap.rs @@ -31,7 +31,6 @@ 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; @@ -91,7 +90,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { let a = a.clone(); let b = b.clone(); @@ -200,7 +199,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 + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { let mut peers: Vec> = Vec::with_capacity(schedule.n_peers); for i in 0..schedule.n_peers { diff --git a/tests/common/peer.rs b/tests/common/peer.rs index f6705a932..6f80c1888 100644 --- a/tests/common/peer.rs +++ b/tests/common/peer.rs @@ -13,7 +13,6 @@ //! observed, matching both the `UnorderedMessages` delivery contract and //! the shadow simulator's model in `schedule::arb`. -use borsh::{BorshDeserialize, BorshSerialize}; use rumors::{Rumors, Version, causally}; use crate::common::wire::{block_on, wire_gossip_async}; @@ -35,7 +34,7 @@ pub struct Peer { 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. @@ -98,7 +97,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 + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { block_on(wire_gossip_async(&a.local, &b.local)); a.drain(); @@ -111,7 +110,7 @@ where /// non-termination guard. pub fn quiesce(peers: &mut [Peer]) where - T: Clone + Eq + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Eq + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { let mut refs: Vec<&mut Peer> = peers.iter_mut().collect(); quiesce_refs(&mut refs); @@ -121,7 +120,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 + serde::Serialize + serde::de::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 +137,7 @@ where /// should catch). fn quiesce_refs(peers: &mut [&mut Peer]) where - T: Clone + Eq + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Eq + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { let n = peers.len(); if n < 2 { diff --git a/tests/common/schedule/executor.rs b/tests/common/schedule/executor.rs index 85e81dcc7..951ed37ec 100644 --- a/tests/common/schedule/executor.rs +++ b/tests/common/schedule/executor.rs @@ -11,7 +11,6 @@ use std::collections::BTreeMap; -use borsh::{BorshDeserialize, BorshSerialize}; use rumors::{Retire, Version}; use super::events::{Event, EventIdx, Schedule}; @@ -64,7 +63,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 + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { execute_with(schedule, windows, |_, _, _| true) } @@ -77,7 +76,7 @@ pub fn execute_and_quiesce( windows: &WindowAssignment, ) -> ExecutionResult where - T: Clone + Eq + Ord + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Eq + Ord + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { let mut result = execute(schedule, windows); quiesce(&mut result.peers); @@ -111,7 +110,7 @@ pub fn execute_with( allow_gossip: F, ) -> ExecutionResult where - T: Clone + Ord + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Ord + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, F: Fn(usize, usize, EventIdx) -> bool, { assert!( @@ -140,7 +139,7 @@ pub fn execute_membership( windows: &WindowAssignment, ) -> MembershipExecutionResult where - T: Clone + Ord + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Ord + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { execute_slots(schedule, windows, |_, _, _| true) } @@ -152,7 +151,7 @@ pub fn execute_membership_and_quiesce( windows: &WindowAssignment, ) -> MembershipExecutionResult where - T: Clone + Eq + Ord + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Eq + Ord + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { let mut result = execute_membership(schedule, windows); quiesce_slots(&mut result.slots); @@ -177,7 +176,7 @@ fn execute_slots( allow_gossip: F, ) -> MembershipExecutionResult where - T: Clone + Ord + BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: Clone + Ord + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, F: Fn(usize, usize, EventIdx) -> bool, { let mut slots: Vec>> = Vec::with_capacity(schedule.n_peers); diff --git a/tests/common/wire.rs b/tests/common/wire.rs index 9c956ca35..e617eb4c9 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}; @@ -121,7 +120,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { block_on(wire_gossip_async(a, b)); } @@ -130,7 +129,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { let _ = gossip_pair_async(a, b).await; } @@ -145,7 +144,7 @@ pub async fn gossip_pair_async( b: &Rumors, ) -> (rumors::Gossiped, rumors::Gossiped) where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { let (mut a_link, mut b_link) = rumors::link::memory_with_capacity(LINK_BUF); @@ -206,7 +205,7 @@ pub async fn divergent_pair( #[track_caller] pub fn bootstrap_fork(parent: &Rumors) -> Rumors where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, { block_on(bootstrap_fork_async_with_protocol(parent, Protocol::V2)) } @@ -215,7 +214,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, { bootstrap_fork_async_with_protocol(parent, Protocol::V2).await } @@ -231,7 +230,7 @@ pub async fn bootstrap_fork_async_with_protocol( protocol: Protocol, ) -> Rumors where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, { bootstrap_fork_configured(parent, protocol, WindowChoice::Floor).await } @@ -241,7 +240,7 @@ where #[track_caller] pub fn bootstrap_fork_with_window(parent: &Rumors, window: WindowChoice) -> Rumors where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, { block_on(bootstrap_fork_with_window_async(parent, window)) } @@ -253,7 +252,7 @@ pub async fn bootstrap_fork_with_window_async( window: WindowChoice, ) -> Rumors where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, { bootstrap_fork_configured(parent, Protocol::V2, window).await } @@ -266,7 +265,7 @@ async fn bootstrap_fork_configured( window: WindowChoice, ) -> Rumors where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: serde::Serialize + serde::de::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..8b64a7e2f 100644 --- a/tests/dispute_wire.rs +++ b/tests/dispute_wire.rs @@ -29,7 +29,6 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::task::{Context, Poll}; -use borsh::{BorshDeserialize, BorshSerialize}; use rand::rngs::SmallRng; use rand::{RngCore, SeedableRng}; use rumors::link::{Connector, Done, Link, LinkParts, MemoryLink}; @@ -167,7 +166,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: serde::Serialize + serde::de::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 +188,7 @@ where /// side of each end. fn session_wire_bytes(a: &Rumors, b: &Rumors) -> usize where - T: BorshSerialize + BorshDeserialize + Send + Sync + 'static, + T: serde::Serialize + serde::de::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 +211,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, { let (left, right) = diverged(mint); let total = session_wire_bytes(&left, &right); diff --git a/tests/hop_trace.rs b/tests/hop_trace.rs index 1f41fb443..9968e650a 100644 --- a/tests/hop_trace.rs +++ b/tests/hop_trace.rs @@ -327,7 +327,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { let trace = Trace::default(); let (mut a_link, mut b_link) = traced_pair(&trace); diff --git a/tests/pairwise.rs b/tests/pairwise.rs index 9f70a84a5..94bb3ac75 100644 --- a/tests/pairwise.rs +++ b/tests/pairwise.rs @@ -18,7 +18,6 @@ mod common; -use borsh::{BorshDeserialize, BorshSerialize}; use proptest::prelude::*; use rumors::{Rumors, Version, causally}; @@ -30,7 +29,7 @@ use crate::common::wire::{bootstrap_fork, wire_gossip}; /// 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 + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, { bootstrap_fork(k) } diff --git a/tests/session_stats.rs b/tests/session_stats.rs index f3e744f36..a10a4e02d 100644 --- a/tests/session_stats.rs +++ b/tests/session_stats.rs @@ -29,7 +29,7 @@ use crate::common::wire::{LINK_BUF, assert_control_drained, block_on, bootstrap_ /// returning both sides' [`Gossiped`]. async fn gossip_pair(a: &Rumors, b: &Rumors) -> (Gossiped, Gossiped) where - T: borsh::BorshSerialize + borsh::BorshDeserialize + Send + Sync + 'static, + T: serde::Serialize + serde::de::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)); diff --git a/tests/single_peer.rs b/tests/single_peer.rs index a221b4d60..19a8e27d5 100644 --- a/tests/single_peer.rs +++ b/tests/single_peer.rs @@ -153,12 +153,12 @@ struct Explosive { fail: bool, } -impl borsh::BorshSerialize for Explosive { - fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { +impl serde::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/tradeoff_probe.rs b/tests/tradeoff_probe.rs index ecf42ea21..36e80892c 100644 --- a/tests/tradeoff_probe.rs +++ b/tests/tradeoff_probe.rs @@ -33,7 +33,6 @@ 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}; @@ -58,7 +57,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: serde::Serialize + serde::de::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 +99,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, { let (left, right) = diverged(budget, mint); let runtime = tokio::runtime::Builder::new_current_thread() @@ -132,7 +131,7 @@ fn run_cells( targets: &[f64], mint: &mut impl FnMut(&mut SmallRng) -> T, ) where - T: BorshSerialize + BorshDeserialize + Send + Sync + Clone + 'static, + T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, { let (envelope, _) = envelope_and_wire_bytes(); let overhead = 28usize; From ba8045c59c8e2a9fee62f46c264e68a13dafd016 Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 21:47:53 -0400 Subject: [PATCH 04/11] wire: re-accept every snapshot for the version-keyed, CBOR-encoded format One deliberate, owner-ruled pre-release format change, named in full: - Leaf paths and every Merkle digest derive from versions alone (the digest VALUES moved; digest count and 24-byte width did not). - Payloads are CBOR: a leaf record's body is one CBOR byte string wrapping the version's canonical bytes, then the payload's CBOR bytes (record BYTE counts moved with borsh-to-CBOR, separably from the keying change, which moves no payload byte). - The greeting's root-fan listing is raw radix-and-digest records, frame-delimited, with no count prefix. - V1's wire atoms (version, message) are single CBOR values inside the unchanged structural framing. - The bookmark on-disk format is version 3: a CBOR payload behind the same magic/version/hash frame. Both bookmark pins move with it, and AGENTS.md's bookmark re-accept rule is restated so a moved frame_empty is definitionally a format change, distinct from the ratified fixture-re-pin class. The dispute-wire law is re-derived from the tests' own byte counts: the per-message intercept is 35 B (was 34 - the record's version atom now carries a one-byte CBOR byte-string header at calibration-corpus version sizes), the design record stays m = 172, so the design-point anchor is 207 B; the default-budget crossover solves to m* = 60 B (was 61) and the BDP-scale u64 window to 65,404 scopes (~4.3x), with the docs' quoted figures and the generated trade-off table re-derived from the same constants. The 5431 B per-dispute envelope is unmoved (node pricing is untouched), verified by its recomputing test. digestshare: 5606 -> 5273 total wire B over the pinned corpus with digest bytes unchanged (1704 B, 71 digests), so the digest share reads 30.4% -> 32.3%. The searched-shape snapshot fixtures re-stage themselves: payload bytes no longer steer paths, so each fixture mints a deterministic pool of versions, searches the pool for the shape its pin requires, and redacts the rest (tests/common/shape.rs); the self-checks still verify every landed shape. gossip_snapshot's both_redact_same_key is renamed both_redact_the_same_message in the same re-accept. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- AGENTS.md | 17 +- ..._bookmark__format__tests__frame_empty.snap | 2 +- ...ark__format__tests__frame_non_trivial.snap | 2 +- src/peer.rs | 29 +- src/testing.rs | 7 +- ...e_snapshot__message_closing_populated.snap | 4 +- ..._snapshot__message_complete_populated.snap | 2 +- ..._snapshot__message_exchange_populated.snap | 7 +- ...ode_root_single_leaf_full_compression.snap | 2 +- ...node_root_two_leaves_branched_at_root.snap | 6 +- ...pshot__node_s_z_full_256_child_branch.snap | 206 ++++--- ...de_s_z_singleton_path_compressed_leaf.snap | 2 +- ...e_snapshot__node_s_z_two_child_branch.snap | 2 +- ...ternating__wire_snapshot__node_z_leaf.snap | 2 +- ...e_snapshot__node_z_leaf_empty_version.snap | 2 +- ...rnating__wire_snapshot__version_empty.snap | 2 +- ...apshot__version_two_parties_ascending.snap | 2 +- ...sts__bounded_corpus_manifest_snapshot.snap | 134 ++-- ...tests__canonical_frame_atlas_snapshot.snap | 134 ++-- ...ror_atlas__codec_error_atlas_snapshot.snap | 16 +- src/tree/mirror/streaming/window.rs | 10 +- src/tree/mirror/streaming/window/tests.rs | 11 +- src/tree/mirror/streaming/window/tradeoff.md | 18 +- tests/common/mod.rs | 1 + tests/common/shape.rs | 100 +++ tests/dispute_wire.rs | 67 +- tests/gossip_snapshot.rs | 176 +++--- tests/hop_trace.rs | 68 ++- tests/opening_supply.rs | 47 +- .../bootstrap_snapshot__empty_provider.snap | 8 +- ...trap_snapshot__mutual_bootstrap_bails.snap | 8 +- ...ootstrap_snapshot__populated_provider.snap | 54 +- .../bootstrap_snapshot__string_payload.snap | 41 +- ...strap_snapshot__v1_populated_provider.snap | 76 ++- ...etric_message_targets_unbatch_the_run.snap | 44 +- .../gossip_snapshot__batched_supply_run.snap | 42 +- ...napshot__both_redact_the_same_message.snap | 54 ++ ...bulk_initiator_ships_opening_supplies.snap | 97 ++- ...gossip_snapshot__converged_forks_noop.snap | 42 +- ...gossip_snapshot__deep_trie_divergence.snap | 575 ++++++++---------- ...shot__early_supplies_honor_redactions.snap | 98 ++- ...hot__empty_pair_converges_immediately.snap | 8 +- .../gossip_snapshot__fork_insert_redact.snap | 64 +- .../gossip_snapshot__one_sided_transfer.snap | 39 +- .../gossip_snapshot__redaction_only.snap | 34 +- ..._same_live_content_divergent_versions.snap | 26 +- .../gossip_snapshot__string_payload.snap | 48 +- ...ossip_snapshot__v1_one_sided_transfer.snap | 59 +- .../retire_snapshot__divergent_retire.snap | 46 +- .../retire_snapshot__empty_retire.snap | 8 +- ...re_snapshot__retire_into_bootstrapper.snap | 39 +- .../retire_snapshot__v1_divergent_retire.snap | 62 +- 52 files changed, 1428 insertions(+), 1222 deletions(-) create mode 100644 tests/common/shape.rs create mode 100644 tests/snapshots/gossip_snapshot__both_redact_the_same_message.snap diff --git a/AGENTS.md b/AGENTS.md index 59caae4f1..d551bf11e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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/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/peer.rs b/src/peer.rs index a0a7a9d7d..f29719d88 100644 --- a/src/peer.rs +++ b/src/peer.rs @@ -381,13 +381,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`). /// @@ -395,10 +395,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. /// @@ -417,27 +417,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. /// @@ -449,7 +450,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 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/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/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/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/window.rs b/src/tree/mirror/streaming/window.rs index 3655a769b..020b38f66 100644 --- a/src/tree/mirror/streaming/window.rs +++ b/src/tree/mirror/streaming/window.rs @@ -201,18 +201,20 @@ 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"))] 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/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/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/dispute_wire.rs b/tests/dispute_wire.rs index 8b64a7e2f..ad7ad5557 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,6 +34,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::task::{Context, Poll}; +use bytes::Bytes; use rand::rngs::SmallRng; use rand::{RngCore, SeedableRng}; use rumors::link::{Connector, Done, Link, LinkParts, MemoryLink}; @@ -62,21 +68,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; @@ -239,9 +254,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 \ @@ -259,23 +274,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", ); } @@ -294,16 +309,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/gossip_snapshot.rs b/tests/gossip_snapshot.rs index 586f978db..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; @@ -25,6 +25,9 @@ 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}; @@ -49,13 +52,6 @@ fn version_for(rumors: &Rumors, value: u64) -> Version { .unwrap_or_else(|| panic!("no live message holds {value}")) } -/// A leaf's tree path: the full-width BLAKE3 hash of its version's -/// canonical bytes. The fixture self-checks read path bytes through this -/// to verify the tree shapes the pinned sessions rely on. -fn leaf_path(version: &Version) -> [u8; 32] { - *blake3::hash(version.as_bytes()).as_bytes() -} - /// Two empty peers: the minimal session. /// /// After the 25-byte preamble @@ -90,34 +86,32 @@ fn one_sided_transfer() { insta::assert_snapshot!(capture_gossip(a, b)); } -/// The two payload values batch-sent into the seeded universe of -/// [`batched_supply_run`]. The fixture requires the two minted leaves' -/// paths (each the hash of its version) to share their first two bytes: -/// 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). -/// The self-check below enforces the shape. -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 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() { +/// 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() @@ -126,11 +120,25 @@ fn batched_supply_run() { [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 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)); } @@ -145,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)); } @@ -187,22 +190,18 @@ fn stream_frames(capture: &str, header: &str) -> Option> { frames } -/// The two payload values batch-sent into the seeded universe of -/// [`bulk_initiator_ships_opening_supplies`]. The fixture requires the two -/// minted leaves' paths to share their first byte with distinct second -/// bytes, so the initiator's one exclusive root child holds a two-leaf -/// subtree whose leaves split one level down. The self-checks below -/// enforce the shape. -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`]. -/// -/// The fixture requires their leaf paths' first bytes to avoid the -/// initiator's exclusive radix, and the extra message makes the responder -/// the larger set, so the subtree holder wins the initiator election. The -/// self-checks below enforce the shape. -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 @@ -216,15 +215,24 @@ const RESPONDER_BALLAST_FROM: u64 = 100; /// 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 apaths: Vec<[u8; 2]> = a @@ -274,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. @@ -298,16 +295,30 @@ 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(&version_for(&b, 1)); - let y = REDACTION_BALLAST_FROM; - b.batch().send(y).send(y + 1).send(y + 2); (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. @@ -346,15 +357,11 @@ fn early_supplies_honor_redactions() { "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); @@ -493,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 { @@ -545,7 +551,7 @@ fn same_live_content_divergent_versions() { /// 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); diff --git a/tests/hop_trace.rs b/tests/hop_trace.rs index 9968e650a..875e93094 100644 --- a/tests/hop_trace.rs +++ b/tests/hop_trace.rs @@ -535,19 +535,75 @@ fn trace_redaction_session() { /// 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); + { + let mut batch = left.batch(); + for value in 0..64u64 { + batch.send(value); + } + } + let mut pool: Vec<(u64, u8)> = left + .snapshot() + .iter() + .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. 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]; + // 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 diff --git a/tests/opening_supply.rs b/tests/opening_supply.rs index d31b541c3..a9b1463c5 100644 --- a/tests/opening_supply.rs +++ b/tests/opening_supply.rs @@ -17,6 +17,7 @@ use rand::rngs::SmallRng; 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,20 +25,16 @@ fn seeded() -> Rumors { Peer::seed_rng(&mut SmallRng::seed_from_u64(0)).into_rumors() } -/// A second message for the staging: the fixture requires its leaf path -/// (the hash of its version) to share its first byte with message `1`'s, -/// so the two sides dispute one root child. -/// -/// The initiator holds both leaves, the -/// responder — forked between the two sends — only the first. The -/// self-checks below enforce the shape. -const DISPUTED_SIBLING_VALUE: u64 = 165; +/// Pool size for the one-byte path search staging the disputed sibling: +/// 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. The fixture -/// requires their leaf paths' first bytes to avoid the disputed radix and -/// makes the responder the larger set, so the disputed-subtree holder -/// initiates. The self-checks below enforce the shape. -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. @@ -64,15 +61,33 @@ 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. A leaf's path is the full-width 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..c125ac8a9 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: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 + child 0x88: 483427dad201088bf07cb3fe2a1ff18f52ac946ab7aa993a + child 0x9a: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 +listing frame: 79 bytes + 0000: 00 00 00 4b 7c 4a 5b 6b + 0008: ab c4 5a b1 c1 37 6a 46 + 0010: 81 fb 8b 72 11 33 9d 26 + 0018: 41 ba ba 9a 23 88 48 34 + 0020: 27 da d2 01 08 8b f0 7c + 0028: b3 fe 2a 1f f1 8f 52 ac + 0030: 94 6a b7 aa 99 3a 9a 26 + 0038: da 9d 7f c5 70 f4 a2 c7 + 0040: 38 29 fe 6b 1c 87 c3 17 + 0048: 6b e8 f7 66 35 c9 c1 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..2647f0f2d 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: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 + child 0x9a: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 +listing frame: 54 bytes + 0000: 00 00 00 32 7c 4a 5b 6b + 0008: ab c4 5a b1 c1 37 6a 46 + 0010: 81 fb 8b 72 11 33 9d 26 + 0018: 41 ba ba 9a 23 9a 26 da + 0020: 9d 7f c5 70 f4 a2 c7 38 + 0028: 29 fe 6b 1c 87 c3 17 6b + 0030: e8 f7 66 35 c9 c1 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..0b0a19331 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 4a 5b 6b ab c4 5a b1 │ 0000: 00 00 00 4f 03 00 00 00 + 0010: c1 37 6a 46 81 fb 8b 72 │ 0008: 7c 4a 5b 6b ab c4 5a b1 + 0018: 11 33 9d 26 41 ba ba 9a │ 0010: c1 37 6a 46 81 fb 8b 72 + 0020: 23 88 48 34 27 da d2 01 │ 0018: 11 33 9d 26 41 ba ba 9a + 0028: 08 8b f0 7c b3 fe 2a 1f │ 0020: 23 88 48 34 27 da d2 01 + 0030: f1 8f 52 ac 94 6a b7 aa │ 0028: 08 8b f0 7c b3 fe 2a 1f + 0038: 99 3a 9a 26 da 9d 7f c5 │ 0030: f1 8f 52 ac 94 6a b7 aa + 0040: 70 f4 a2 c7 38 29 fe 6b │ 0038: 99 3a 9a 26 da 9d 7f c5 + 0048: 1c 87 c3 17 6b e8 f7 66 │ 0040: 70 f4 a2 c7 38 29 fe 6b + 0050: 35 c9 c1 │ 0048: 1c 87 c3 17 6b e8 f7 66 +received 19 bytes │ 0050: 35 c9 c1 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..46399a8f7 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: 97fdeb2f910d20fbe119f2671eb624cf9c8417143a2435d4 +listing frame: 29 bytes + 0000: 00 00 00 19 79 97 fd eb + 0008: 2f 91 0d 20 fb e1 19 f2 + 0010: 67 1e b6 24 cf 9c 84 17 + 0018: 14 3a 24 35 d4 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..219ece854 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: 97fdeb2f910d20fbe119f2671eb624cf9c8417143a2435d4 +listing frame: 29 bytes + 0000: 00 00 00 19 79 97 fd eb + 0008: 2f 91 0d 20 fb e1 19 f2 + 0010: 67 1e b6 24 cf 9c 84 17 + 0018: 14 3a 24 35 d4 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..6f94e1006 --- /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: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 +listing frame: 29 bytes + 0000: 00 00 00 19 7c 4a 5b 6b + 0008: ab c4 5a b1 c1 37 6a 46 + 0010: 81 fb 8b 72 11 33 9d 26 + 0018: 41 ba ba 9a 23 +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: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 +listing frame: 29 bytes + 0000: 00 00 00 19 7c 4a 5b 6b + 0008: ab c4 5a b1 c1 37 6a 46 + 0010: 81 fb 8b 72 11 33 9d 26 + 0018: 41 ba ba 9a 23 +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..1466cebd0 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: 176df43d3144011ce7429832cad9212cf152a4d343757e68 +listing frame: 29 bytes + 0000: 00 00 00 19 1b 17 6d f4 + 0008: 3d 31 44 01 1c e7 42 98 + 0010: 32 ca d9 21 2c f1 52 a4 + 0018: d3 43 75 7e 68 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: f03024f7dbe56599296b2eb532822f29be44c48777943703 + child 0x36: 9ddf089f8c85ffebaf8a346d1af381f138082ce95f276640 + child 0xf2: 9b77e29e06b3bb072170d1851eb26c6007fd01cbd5c9a2f7 +listing frame: 79 bytes + 0000: 00 00 00 4b 1a f0 30 24 + 0008: f7 db e5 65 99 29 6b 2e + 0010: b5 32 82 2f 29 be 44 c4 + 0018: 87 77 94 37 03 36 9d df + 0020: 08 9f 8c 85 ff eb af 8a + 0028: 34 6d 1a f3 81 f1 38 08 + 0030: 2c e9 5f 27 66 40 f2 9b + 0038: 77 e2 9e 06 b3 bb 07 21 + 0040: 70 d1 85 1e b2 6c 60 07 + 0048: fd 01 cb d5 c9 a2 f7 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..009683b63 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: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 + child 0x9a: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 +listing frame: 54 bytes + 0000: 00 00 00 32 7c 4a 5b 6b + 0008: ab c4 5a b1 c1 37 6a 46 + 0010: 81 fb 8b 72 11 33 9d 26 + 0018: 41 ba ba 9a 23 9a 26 da + 0020: 9d 7f c5 70 f4 a2 c7 38 + 0028: 29 fe 6b 1c 87 c3 17 6b + 0030: e8 f7 66 35 c9 c1 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: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 + child 0x9a: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 +listing frame: 54 bytes + 0000: 00 00 00 32 7c 4a 5b 6b + 0008: ab c4 5a b1 c1 37 6a 46 + 0010: 81 fb 8b 72 11 33 9d 26 + 0018: 41 ba ba 9a 23 9a 26 da + 0020: 9d 7f c5 70 f4 a2 c7 38 + 0028: 29 fe 6b 1c 87 c3 17 6b + 0030: e8 f7 66 35 c9 c1 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..1fa387d21 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: 9706f21cc160f366a0857b586b863c58a39f11d2864d7c0c + child 0x1b: a6849e73f4415befd030b3a2534e3dfc044e651c5c871f9a + child 0x21: e0bac08691f3a1bd81a3b9922407177bf6444cfaa4dee952 + child 0x4b: 91e9ec619218a54fb52023da657470cd6fc3cc437cb64bf7 + child 0x66: 08b51b7fd019850239388c3cc11f68902d350b46651bd793 + child 0x6c: 76aed85cfc9f76a4ad7110dce5cdc3a9fe31aa393f0508ac + child 0x6f: f563da5d17469396480e9d82ddde7a0154a9d8b471d4f029 + child 0x87: fa2cae4ddab64d8a20951bac33c328218ed7fb4a25de4af5 + child 0x93: 00b9252e54561ed3e303a1413e6f1d8b1115bd578a905323 + child 0xa0: cb4eca4fd3dc50fb7ac7f18c4acaa47b9802fff0dbf72a08 + child 0xb4: 7697b4614ab59c3954130a7a358dc8b49038a565c3e7b488 + child 0xb7: d0584bc370a49dafed3633b6232b07e71014dc89a92fb110 + child 0xc4: abda8abdc1256b7a24333e73af3f71de68a8331f368d3cae + child 0xd0: 0d61af285f22355ed1e9cd8481f53092ea95de4dfdc56e42 + child 0xf3: e35b9d171bc5412736bbec70ba5cc93ee5e4516ce8b125d1 + child 0xf5: d92b43bfe7eb0b6b56c71181959b9e8881f80e6c2198adf7 +listing frame: 404 bytes + 0000: 00 00 01 90 04 97 06 f2 + 0008: 1c c1 60 f3 66 a0 85 7b + 0010: 58 6b 86 3c 58 a3 9f 11 + 0018: d2 86 4d 7c 0c 1b a6 84 + 0020: 9e 73 f4 41 5b ef d0 30 + 0028: b3 a2 53 4e 3d fc 04 4e + 0030: 65 1c 5c 87 1f 9a 21 e0 + 0038: ba c0 86 91 f3 a1 bd 81 + 0040: a3 b9 92 24 07 17 7b f6 + 0048: 44 4c fa a4 de e9 52 4b + 0050: 91 e9 ec 61 92 18 a5 4f + 0058: b5 20 23 da 65 74 70 cd + 0060: 6f c3 cc 43 7c b6 4b f7 + 0068: 66 08 b5 1b 7f d0 19 85 + 0070: 02 39 38 8c 3c c1 1f 68 + 0078: 90 2d 35 0b 46 65 1b d7 + 0080: 93 6c 76 ae d8 5c fc 9f + 0088: 76 a4 ad 71 10 dc e5 cd + 0090: c3 a9 fe 31 aa 39 3f 05 + 0098: 08 ac 6f f5 63 da 5d 17 + 00a0: 46 93 96 48 0e 9d 82 dd + 00a8: de 7a 01 54 a9 d8 b4 71 + 00b0: d4 f0 29 87 fa 2c ae 4d + 00b8: da b6 4d 8a 20 95 1b ac + 00c0: 33 c3 28 21 8e d7 fb 4a + 00c8: 25 de 4a f5 93 00 b9 25 + 00d0: 2e 54 56 1e d3 e3 03 a1 + 00d8: 41 3e 6f 1d 8b 11 15 bd + 00e0: 57 8a 90 53 23 a0 cb 4e + 00e8: ca 4f d3 dc 50 fb 7a c7 + 00f0: f1 8c 4a ca a4 7b 98 02 + 00f8: ff f0 db f7 2a 08 b4 76 + 0100: 97 b4 61 4a b5 9c 39 54 + 0108: 13 0a 7a 35 8d c8 b4 90 + 0110: 38 a5 65 c3 e7 b4 88 b7 + 0118: d0 58 4b c3 70 a4 9d af + 0120: ed 36 33 b6 23 2b 07 e7 + 0128: 10 14 dc 89 a9 2f b1 10 + 0130: c4 ab da 8a bd c1 25 6b + 0138: 7a 24 33 3e 73 af 3f 71 + 0140: de 68 a8 33 1f 36 8d 3c + 0148: ae d0 0d 61 af 28 5f 22 + 0150: 35 5e d1 e9 cd 84 81 f5 + 0158: 30 92 ea 95 de 4d fd c5 + 0160: 6e 42 f3 e3 5b 9d 17 1b + 0168: c5 41 27 36 bb ec 70 ba + 0170: 5c c9 3e e5 e4 51 6c e8 + 0178: b1 25 d1 f5 d9 2b 43 bf + 0180: e7 eb 0b 6b 56 c7 11 81 + 0188: 95 9b 9e 88 81 f8 0e 6c + 0190: 21 98 ad f7 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: f03024f7dbe56599296b2eb532822f29be44c48777943703 + child 0x1e: 146bd6081df473f2416d0bde6d5f02c98e3b716094270b0f + child 0x36: 9ddf089f8c85ffebaf8a346d1af381f138082ce95f276640 + child 0x47: 43189a95d8130f683dbf72f4dd6699fd6f44ee56472a967e + child 0x5b: 2515879992e8c053e0ad7818be3a3a1100a77fdfcfc59857 + child 0x5d: 88eb18314907a0f9a42ab7bfa0ee94335a57a9732f427f17 + child 0x5e: 67f39e1d23f94021cd8718f9ba8b1c0826de3a384963189f + child 0x74: 8deb535ac2e690d1254c5982dfd6176f3e55263b82b80028 + child 0x83: 89c2ca0a6098161085a6fb172f4a5898a03fe5d7d7c79d88 + child 0x9b: 897bbd87fe13d0ccc702570c2cd7a8cb8a689a87c530f2eb + child 0xc0: 1f47039fd08e6128872926842a157b7499599ff4ced098ab + child 0xc6: ab8d77b591864027d16445c97070bfdff2809d812393640d + child 0xe9: 28b444ce804288129d2ccafeef605842e9e978f1e475ddd1 + child 0xf2: 9b77e29e06b3bb072170d1851eb26c6007fd01cbd5c9a2f7 + child 0xf8: 309d1bc3b1291b1e1c2b83f34d2dfa13f14083c9b1032da6 +listing frame: 379 bytes + 0000: 00 00 01 77 1a f0 30 24 + 0008: f7 db e5 65 99 29 6b 2e + 0010: b5 32 82 2f 29 be 44 c4 + 0018: 87 77 94 37 03 1e 14 6b + 0020: d6 08 1d f4 73 f2 41 6d + 0028: 0b de 6d 5f 02 c9 8e 3b + 0030: 71 60 94 27 0b 0f 36 9d + 0038: df 08 9f 8c 85 ff eb af + 0040: 8a 34 6d 1a f3 81 f1 38 + 0048: 08 2c e9 5f 27 66 40 47 + 0050: 43 18 9a 95 d8 13 0f 68 + 0058: 3d bf 72 f4 dd 66 99 fd + 0060: 6f 44 ee 56 47 2a 96 7e + 0068: 5b 25 15 87 99 92 e8 c0 + 0070: 53 e0 ad 78 18 be 3a 3a + 0078: 11 00 a7 7f df cf c5 98 + 0080: 57 5d 88 eb 18 31 49 07 + 0088: a0 f9 a4 2a b7 bf a0 ee + 0090: 94 33 5a 57 a9 73 2f 42 + 0098: 7f 17 5e 67 f3 9e 1d 23 + 00a0: f9 40 21 cd 87 18 f9 ba + 00a8: 8b 1c 08 26 de 3a 38 49 + 00b0: 63 18 9f 74 8d eb 53 5a + 00b8: c2 e6 90 d1 25 4c 59 82 + 00c0: df d6 17 6f 3e 55 26 3b + 00c8: 82 b8 00 28 83 89 c2 ca + 00d0: 0a 60 98 16 10 85 a6 fb + 00d8: 17 2f 4a 58 98 a0 3f e5 + 00e0: d7 d7 c7 9d 88 9b 89 7b + 00e8: bd 87 fe 13 d0 cc c7 02 + 00f0: 57 0c 2c d7 a8 cb 8a 68 + 00f8: 9a 87 c5 30 f2 eb c0 1f + 0100: 47 03 9f d0 8e 61 28 87 + 0108: 29 26 84 2a 15 7b 74 99 + 0110: 59 9f f4 ce d0 98 ab c6 + 0118: ab 8d 77 b5 91 86 40 27 + 0120: d1 64 45 c9 70 70 bf df + 0128: f2 80 9d 81 23 93 64 0d + 0130: e9 28 b4 44 ce 80 42 88 + 0138: 12 9d 2c ca fe ef 60 58 + 0140: 42 e9 e9 78 f1 e4 75 dd + 0148: d1 f2 9b 77 e2 9e 06 b3 + 0150: bb 07 21 70 d1 85 1e b2 + 0158: 6c 60 07 fd 01 cb d5 c9 + 0160: a2 f7 f8 30 9d 1b c3 b1 + 0168: 29 1b 1e 1c 2b 83 f3 4d + 0170: 2d fa 13 f1 40 83 c9 b1 + 0178: 03 2d a6 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..029a5fbae 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: a04d4d00cd54506f37c22d58d5321e135fdc27778707128e +listing frame: 29 bytes + 0000: 00 00 00 19 9a a0 4d 4d + 0008: 00 cd 54 50 6f 37 c2 2d + 0010: 58 d5 32 1e 13 5f dc 27 + 0018: 77 87 07 12 8e 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: d4cf592c487d3a67dd368e1a0c450b8c45088d07aae68a9a + child 0xab: bfc70672bfd0ac57279fb33afa6234203b54a984e2a63eb0 + child 0xfa: a3d9effce8b2ffb768b9ea4291a7068171652e7199da7f5c +listing frame: 79 bytes + 0000: 00 00 00 4b 90 d4 cf 59 + 0008: 2c 48 7d 3a 67 dd 36 8e + 0010: 1a 0c 45 0b 8c 45 08 8d + 0018: 07 aa e6 8a 9a ab bf c7 + 0020: 06 72 bf d0 ac 57 27 9f + 0028: b3 3a fa 62 34 20 3b 54 + 0030: a9 84 e2 a6 3e b0 fa a3 + 0038: d9 ef fc e8 b2 ff b7 68 + 0040: b9 ea 42 91 a7 06 81 71 + 0048: 65 2e 71 99 da 7f 5c 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..2148f4e24 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: 76c57e179acd80b1cd3d2f2db7364a23fac273a7ba43a53a + child 0x7c: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 +listing frame: 54 bytes + 0000: 00 00 00 32 64 76 c5 7e + 0008: 17 9a cd 80 b1 cd 3d 2f + 0010: 2d b7 36 4a 23 fa c2 73 + 0018: a7 ba 43 a5 3a 7c 4a 5b + 0020: 6b ab c4 5a b1 c1 37 6a + 0028: 46 81 fb 8b 72 11 33 9d + 0030: 26 41 ba ba 9a 23 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: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 + child 0xe7: 726751de5c398bfebd9dd3ad534412578f5784c45147a879 +listing frame: 54 bytes + 0000: 00 00 00 32 9a 26 da 9d + 0008: 7f c5 70 f4 a2 c7 38 29 + 0010: fe 6b 1c 87 c3 17 6b e8 + 0018: f7 66 35 c9 c1 e7 72 67 + 0020: 51 de 5c 39 8b fe bd 9d + 0028: d3 ad 53 44 12 57 8f 57 + 0030: 84 c4 51 47 a8 79 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..d2109276a 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: 00b9252e54561ed3e303a1413e6f1d8b1115bd578a905323 + child 0xa0: cb4eca4fd3dc50fb7ac7f18c4acaa47b9802fff0dbf72a08 +listing frame: 54 bytes + 0000: 00 00 00 32 93 00 b9 25 + 0008: 2e 54 56 1e d3 e3 03 a1 + 0010: 41 3e 6f 1d 8b 11 15 bd + 0018: 57 8a 90 53 23 a0 cb 4e + 0020: ca 4f d3 dc 50 fb 7a c7 + 0028: f1 8c 4a ca a4 7b 98 02 + 0030: ff f0 db f7 2a 08 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..dcb3bba7d 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: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 +listing frame: 29 bytes + 0000: 00 00 00 19 7c 4a 5b 6b + 0008: ab c4 5a b1 c1 37 6a 46 + 0010: 81 fb 8b 72 11 33 9d 26 + 0018: 41 ba ba 9a 23 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: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 + child 0x9a: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 +listing frame: 54 bytes + 0000: 00 00 00 32 7c 4a 5b 6b + 0008: ab c4 5a b1 c1 37 6a 46 + 0010: 81 fb 8b 72 11 33 9d 26 + 0018: 41 ba ba 9a 23 9a 26 da + 0020: 9d 7f c5 70 f4 a2 c7 38 + 0028: 29 fe 6b 1c 87 c3 17 6b + 0030: e8 f7 66 35 c9 c1 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..bd42e9a0f 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: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 +listing frame: 29 bytes + 0000: 00 00 00 19 9a 26 da 9d + 0008: 7f c5 70 f4 a2 c7 38 29 + 0010: fe 6b 1c 87 c3 17 6b e8 + 0018: f7 66 35 c9 c1 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: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 +listing frame: 29 bytes + 0000: 00 00 00 19 9a 26 da 9d + 0008: 7f c5 70 f4 a2 c7 38 29 + 0010: fe 6b 1c 87 c3 17 6b e8 + 0018: f7 66 35 c9 c1 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..00d4d4077 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: 00b9252e54561ed3e303a1413e6f1d8b1115bd578a905323 +listing frame: 29 bytes + 0000: 00 00 00 19 93 00 b9 25 + 0008: 2e 54 56 1e d3 e3 03 a1 + 0010: 41 3e 6f 1d 8b 11 15 bd + 0018: 57 8a 90 53 23 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: 9b77e29e06b3bb072170d1851eb26c6007fd01cbd5c9a2f7 +listing frame: 29 bytes + 0000: 00 00 00 19 f2 9b 77 e2 + 0008: 9e 06 b3 bb 07 21 70 d1 + 0010: 85 1e b2 6c 60 07 fd 01 + 0018: cb d5 c9 a2 f7 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..6161d0f2f 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 00 b9 25 2e 54 56 1e │ 0000: 00 00 00 36 02 00 00 00 + 0010: d3 e3 03 a1 41 3e 6f 1d │ 0008: 93 00 b9 25 2e 54 56 1e + 0018: 8b 11 15 bd 57 8a 90 53 │ 0010: d3 e3 03 a1 41 3e 6f 1d + 0020: 23 a0 cb 4e ca 4f d3 dc │ 0018: 8b 11 15 bd 57 8a 90 53 + 0028: 50 fb 7a c7 f1 8c 4a ca │ 0020: 23 a0 cb 4e ca 4f d3 dc + 0030: a4 7b 98 02 ff f0 db f7 │ 0028: 50 fb 7a c7 f1 8c 4a ca + 0038: 2a 08 │ 0030: a4 7b 98 02 ff f0 db f7 +received 18 bytes │ 0038: 2a 08 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..59aa8e157 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: 00b9252e54561ed3e303a1413e6f1d8b1115bd578a905323 +listing frame: 29 bytes + 0000: 00 00 00 19 93 00 b9 25 + 0008: 2e 54 56 1e d3 e3 03 a1 + 0010: 41 3e 6f 1d 8b 11 15 bd + 0018: 57 8a 90 53 23 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: 9b77e29e06b3bb072170d1851eb26c6007fd01cbd5c9a2f7 +listing frame: 29 bytes + 0000: 00 00 00 19 f2 9b 77 e2 + 0008: 9e 06 b3 bb 07 21 70 d1 + 0010: 85 1e b2 6c 60 07 fd 01 + 0018: cb d5 c9 a2 f7 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..33e99cd38 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: 9ddf089f8c85ffebaf8a346d1af381f138082ce95f276640 + child 0xf2: 9b77e29e06b3bb072170d1851eb26c6007fd01cbd5c9a2f7 +listing frame: 54 bytes + 0000: 00 00 00 32 36 9d df 08 + 0008: 9f 8c 85 ff eb af 8a 34 + 0010: 6d 1a f3 81 f1 38 08 2c + 0018: e9 5f 27 66 40 f2 9b 77 + 0020: e2 9e 06 b3 bb 07 21 70 + 0028: d1 85 1e b2 6c 60 07 fd + 0030: 01 cb d5 c9 a2 f7 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..0c927c02b 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 a7 a6 │ sent 32 bytes + 0010: aa 34 c1 8b cb d1 59 9e │ 0000: 00 00 00 1c 01 00 00 00 + 0018: 58 54 81 6c f0 8b b3 1b │ 0008: a7 a6 aa 34 c1 8b cb d1 + 0020: 88 db a7 6e 7c 34 │ 0010: 59 9e 58 54 81 6c f0 8b +sent 33 bytes │ 0018: b3 1b 88 db a7 6e 7c 34 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 00 b9 25 2e 54 56 1e │ 0000: 00 00 00 1d 01 00 00 00 + 0010: d3 e3 03 a1 41 3e 6f 1d │ 0008: 93 00 b9 25 2e 54 56 1e + 0018: 8b 11 15 bd 57 8a 90 53 │ 0010: d3 e3 03 a1 41 3e 6f 1d + 0020: 23 │ 0018: 8b 11 15 bd 57 8a 90 53 +received 54 bytes │ 0020: 23 + 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 From acb556fc618425c6b3eadadf5b5f92f1788215a2 Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 21:48:09 -0400 Subject: [PATCH 05/11] docs: speak version addressing everywhere, and name the payload contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vocabulary sweep retargets every remaining content-addressing claim to what the tree does: leaves are version-addressed, paths and digests are version-derived, and the uniformity arguments (window statistics, descent depth) rest on version hashing. The full-width identity primitive is renamed PathHash — it hashes a version into a leaf's path, and nothing about it is a content hash. The crate docs gain a "Message payloads" section stating the serde/CBOR contract callers now hold: names are the evolution contract, unknown fields skip, missing fields error absent serde defaults, and no canonical encoding is required of T because payload bytes carry no identity. READMEs regenerated (`just readme`); both rustdoc gates (public and private) run clean. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- README.md | 21 ++++++++++++---- benches/in_memory.rs | 2 +- benches/support/grid.rs | 6 ++--- examples/swarm.rs | 6 ++--- src/bookmark/format.rs | 2 +- src/conformance/link.rs | 2 +- src/error.rs | 4 ++-- src/lib.rs | 13 ++++++++++ src/peer.rs | 2 +- src/peer/gossip/tests.rs | 2 +- src/rumors/causal.rs | 5 ++-- src/tree/arb.rs | 10 ++++---- src/tree/mirror/alternating/message.rs | 8 +++---- src/tree/mirror/alternating/message/tests.rs | 2 +- src/tree/mirror/alternating/tests.rs | 2 +- .../streaming/materialized/work/answer.rs | 4 ++-- .../streaming/materialized/work/levels.rs | 2 +- src/tree/mirror/streaming/remote.rs | 15 ++++++------ src/tree/mirror/streaming/remote/adapter.rs | 2 +- .../mirror/streaming/remote/adapter/decode.rs | 4 ++-- .../mirror/streaming/remote/adapter/error.rs | 4 ++-- .../remote/adapter/tests/malformed.rs | 2 +- .../streaming/remote/adapter/tests/opening.rs | 4 ++-- .../streaming/remote/adapter/tests/parking.rs | 2 +- .../remote/adapter/tests/properties.rs | 6 ++--- src/tree/mirror/streaming/remote/codec.rs | 2 +- .../streaming/remote/codec/capture/tests.rs | 4 ++++ .../mirror/streaming/remote/codec/error.rs | 2 +- .../streaming/remote/proxy/start/tests.rs | 4 ++-- .../mirror/streaming/remote/proxy/tests.rs | 2 +- src/tree/mirror/streaming/tests/wedge.rs | 2 +- src/tree/mirror/streaming/window.rs | 14 ++++++----- src/tree/tests.rs | 22 ++++++++++------- src/tree/traverse/join.rs | 2 +- src/tree/typed/hash.rs | 24 +++++++++---------- src/tree/typed/hash/tests.rs | 4 ++-- src/tree/typed/height.rs | 2 +- src/tree/typed/path.rs | 8 +++---- src/tree/typed/untyped.rs | 3 ++- tests/async_wire.rs | 4 ++-- tests/bookmark_causality.rs | 2 +- tests/bootstrap.rs | 2 +- tests/bootstrap_snapshot.rs | 8 +++---- tests/common/oracle.rs | 8 ++++--- tests/common/schedule/events.rs | 6 +++-- tests/multi_peer.rs | 2 +- tests/opening_supply.rs | 5 ++-- 47 files changed, 154 insertions(+), 110 deletions(-) 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/in_memory.rs b/benches/in_memory.rs index e0c6a43f7..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. //! diff --git a/benches/support/grid.rs b/benches/support/grid.rs index f3c6dada1..5d3e76851 100644 --- a/benches/support/grid.rs +++ b/benches/support/grid.rs @@ -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 { diff --git a/examples/swarm.rs b/examples/swarm.rs index b0abb3c5f..331a455b2 100644 --- a/examples/swarm.rs +++ b/examples/swarm.rs @@ -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 diff --git a/src/bookmark/format.rs b/src/bookmark/format.rs index ec396803a..22809a333 100644 --- a/src/bookmark/format.rs +++ b/src/bookmark/format.rs @@ -18,7 +18,7 @@ //! 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 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 7145ddcd6..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,7 +52,7 @@ 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] std::io::Error), diff --git a/src/lib.rs b/src/lib.rs index e005450e2..2b17e5d04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -230,6 +230,19 @@ //! [`AsyncRead`](tokio::io::AsyncRead) and [`AsyncWrite`](tokio::io::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/src/peer.rs b/src/peer.rs index f29719d88..b1923e747 100644 --- a/src/peer.rs +++ b/src/peer.rs @@ -335,7 +335,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. diff --git a/src/peer/gossip/tests.rs b/src/peer/gossip/tests.rs index a9e281c34..9d9b6dc08 100644 --- a/src/peer/gossip/tests.rs +++ b/src/peer/gossip/tests.rs @@ -241,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/rumors/causal.rs b/src/rumors/causal.rs index 97c674d79..caac9c337 100644 --- a/src/rumors/causal.rs +++ b/src/rumors/causal.rs @@ -46,8 +46,9 @@ pub struct CausalMessages { /// staged, undelivered message nor the delivered message still in the /// caller's hands. checkpoint: Version, - /// The undelivered backlog in rank-then-canonical-bytes order — the - /// same total order as [`before::Ranked`], with the [`Rank`] + /// 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 diff --git a/src/tree/arb.rs b/src/tree/arb.rs index d7e029ff5..501ed15c3 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. @@ -186,7 +186,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 @@ -431,7 +431,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 @@ -513,7 +513,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 { @@ -539,7 +539,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, diff --git a/src/tree/mirror/alternating/message.rs b/src/tree/mirror/alternating/message.rs index d8c593c2c..9d53a24db 100644 --- a/src/tree/mirror/alternating/message.rs +++ b/src/tree/mirror/alternating/message.rs @@ -1,6 +1,6 @@ //! # Wire format //! -//! Each message is encoded by the tree's [`wire`](crate::tree::wire) +//! 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. //! @@ -23,9 +23,9 @@ //! //! ## 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 ::= diff --git a/src/tree/mirror/alternating/message/tests.rs b/src/tree/mirror/alternating/message/tests.rs index 25aac4da8..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 diff --git a/src/tree/mirror/alternating/tests.rs b/src/tree/mirror/alternating/tests.rs index a28e1d37e..9e70f8885 100644 --- a/src/tree/mirror/alternating/tests.rs +++ b/src/tree/mirror/alternating/tests.rs @@ -225,7 +225,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<_> { diff --git a/src/tree/mirror/streaming/materialized/work/answer.rs b/src/tree/mirror/streaming/materialized/work/answer.rs index 6b77ac676..e65a84020 100644 --- a/src/tree/mirror/streaming/materialized/work/answer.rs +++ b/src/tree/mirror/streaming/materialized/work/answer.rs @@ -108,7 +108,7 @@ where /// Answer one leaf-parent query by merge-joining both leaf listings. /// /// The leaf-parent twin of [`internal`]'s dispute chokepoint: a matching -/// radix here always agrees (paths are content-addressed, so equal path +/// radix here always agrees (paths are version-derived, so equal path /// means equal leaf), so the scope was disputed exactly when both listings /// were non-empty and some leaf sat on one side alone. Each exclusive local /// leaf the causal filter drops is one deletion honored @@ -172,7 +172,7 @@ where /// Answer one terminal leaf query. /// /// A terminal leaf question is always a *request* (leaves cannot be -/// disputed: equal content-addressed path means equal leaf, and a non-empty +/// disputed: equal version-derived path means equal leaf, and a non-empty /// listing here is a protocol violation), so no dispute is counted. A /// requested leaf the causal filter drops is one deletion honored /// ([`messages_shed`](crate::SessionStats::messages_shed)). diff --git a/src/tree/mirror/streaming/materialized/work/levels.rs b/src/tree/mirror/streaming/materialized/work/levels.rs index 9b181abd9..3829b7a62 100644 --- a/src/tree/mirror/streaming/materialized/work/levels.rs +++ b/src/tree/mirror/streaming/materialized/work/levels.rs @@ -428,7 +428,7 @@ where (self.respond(responses), asked_rx, upper_rx, lower_rx) } - /// Walk leaf parents, where disputes compare content-addressed leaves. + /// Walk leaf parents, where disputes compare version-addressed leaves. pub fn leaf_parent_level( &mut self, their_version: Version, diff --git a/src/tree/mirror/streaming/remote.rs b/src/tree/mirror/streaming/remote.rs index 1a646b598..9d9b6b92d 100644 --- a/src/tree/mirror/streaming/remote.rs +++ b/src/tree/mirror/streaming/remote.rs @@ -31,13 +31,14 @@ //! count-minus-one admits every fan from 1 through 256. //! //! Supplied leaves ship in *runs*: one exact-length-delimited body carrying -//! one or more leaf records, each itself an exact-length-delimited -//! canonical borsh encoding of its [`Version`](crate::Version) and -//! [`Message`](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 f1e93ca34..660fbc966 100644 --- a/src/tree/mirror/streaming/remote/adapter/decode.rs +++ b/src/tree/mirror/streaming/remote/adapter/decode.rs @@ -64,7 +64,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 @@ -444,7 +444,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) } 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/malformed.rs b/src/tree/mirror/streaming/remote/adapter/tests/malformed.rs index 522b58c06..cc213e333 100644 --- a/src/tree/mirror/streaming/remote/adapter/tests/malformed.rs +++ b/src/tree/mirror/streaming/remote/adapter/tests/malformed.rs @@ -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(); 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 8fd5c6221..9668b01a1 100644 --- a/src/tree/mirror/streaming/remote/adapter/tests/parking.rs +++ b/src/tree/mirror/streaming/remote/adapter/tests/parking.rs @@ -85,7 +85,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/tests.rs b/src/tree/mirror/streaming/remote/codec/capture/tests.rs index 6858cce19..bff292e1b 100644 --- a/src/tree/mirror/streaming/remote/codec/capture/tests.rs +++ b/src/tree/mirror/streaming/remote/codec/capture/tests.rs @@ -30,6 +30,10 @@ fn encode_listing(children: &[(u8, Hash)]) -> Vec { body } +/// The capture renderer decodes each supply record structurally: two runs +/// differing only in the record's version render record lines that differ +/// exactly at the named version field, with the record count and the +/// (identical) payload accounting unchanged. #[test] fn supply_decode_names_the_field_that_moved() { let party = before::Party::seed(); diff --git a/src/tree/mirror/streaming/remote/codec/error.rs b/src/tree/mirror/streaming/remote/codec/error.rs index ea34d9db7..eb9befe91 100644 --- a/src/tree/mirror/streaming/remote/codec/error.rs +++ b/src/tree/mirror/streaming/remote/codec/error.rs @@ -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 diff --git a/src/tree/mirror/streaming/remote/proxy/start/tests.rs b/src/tree/mirror/streaming/remote/proxy/start/tests.rs index b9ae5bfea..7fbe5fb35 100644 --- a/src/tree/mirror/streaming/remote/proxy/start/tests.rs +++ b/src/tree/mirror/streaming/remote/proxy/start/tests.rs @@ -190,7 +190,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. @@ -259,7 +259,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 diff --git a/src/tree/mirror/streaming/remote/proxy/tests.rs b/src/tree/mirror/streaming/remote/proxy/tests.rs index d92b811d9..37679cd44 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests.rs @@ -270,7 +270,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/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 020b38f66..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,9 +201,11 @@ 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 -/// (the record's version atom rides as a CBOR byte string, whose header -/// is part of this intercept). +/// 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 @@ -551,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/tests.rs b/src/tree/tests.rs index 6d7813502..a999848e5 100644 --- a/src/tree/tests.rs +++ b/src/tree/tests.rs @@ -658,10 +658,12 @@ proptest! { } /// `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, - /// and it rides the identical-leaf arm: a re-delivered insert matches - /// the resident leaf byte-for-byte and is kept, never a collision. + /// 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(); @@ -1187,7 +1189,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. @@ -1570,7 +1572,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( @@ -1862,9 +1864,11 @@ fn join_detects_a_version_collision_at_one_path() { } /// Two leaves carrying the *same version* with different payloads compare -/// digest-equal — digests are content-blind — so `Tree::join` keeps one -/// side and reports no change: the modeled trade, pinned so its boundary -/// with the detected (version-mismatch) case stays explicit. +/// 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 the detected (version-mismatch) case stays +/// explicit. #[test] fn join_prunes_same_version_payload_divergence_as_equal() { let version = version_for("A", 1); diff --git a/src/tree/traverse/join.rs b/src/tree/traverse/join.rs index e3b9943da..5eb3b8f98 100644 --- a/src/tree/traverse/join.rs +++ b/src/tree/traverse/join.rs @@ -24,7 +24,7 @@ //! - **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). //! diff --git a/src/tree/typed/hash.rs b/src/tree/typed/hash.rs index 422451ca3..435581d3b 100644 --- a/src/tree/typed/hash.rs +++ b/src/tree/typed/hash.rs @@ -18,7 +18,7 @@ pub const MERKLE_HASH_LEN: usize = 24; /// [`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 @@ -80,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`: @@ -242,23 +242,23 @@ impl From for [u8; MERKLE_HASH_LEN] { } } -/// Full-width 32-byte BLAKE3 hash: the identity 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'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 `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]); +/// 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. @@ -278,8 +278,8 @@ impl ContentHash { } } -impl From for [u8; 32] { - fn from(hash: ContentHash) -> Self { +impl From for [u8; 32] { + fn from(hash: PathHash) -> Self { hash.0 } } diff --git a/src/tree/typed/hash/tests.rs b/src/tree/typed/hash/tests.rs index 4adfb3b7b..beef9f733 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)*`. @@ -142,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/path.rs b/src/tree/typed/path.rs index 705d11bb6..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; +use super::hash::PathHash; use super::height::{Height, Root, S}; use crate::Version; @@ -28,14 +28,14 @@ impl Path { /// 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 `ContentHash`, never the + /// The path is the full-width 32-byte `PathHash`, never the /// truncated Merkle `Hash`: a path collision is permanent split-brain - /// (see `ContentHash`). The preimage is one self-delimiting canonical + /// (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: ContentHash::of(version.as_bytes()).into(), + hash: PathHash::of(version.as_bytes()).into(), } } } diff --git a/src/tree/typed/untyped.rs b/src/tree/typed/untyped.rs index 642756e16..730809765 100644 --- a/src/tree/typed/untyped.rs +++ b/src/tree/typed/untyped.rs @@ -653,7 +653,8 @@ impl Node { /// Serialize the node in its in-memory layout. /// - /// This is the canonical encoder: the typed [`wire::Encode`] 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. diff --git a/tests/async_wire.rs b/tests/async_wire.rs index 9f2dc9c22..f2d4e1020 100644 --- a/tests/async_wire.rs +++ b/tests/async_wire.rs @@ -12,7 +12,7 @@ //! //! 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 063263e48..036155ebc 100644 --- a/tests/bookmark_causality.rs +++ b/tests/bookmark_causality.rs @@ -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 diff --git a/tests/bootstrap.rs b/tests/bootstrap.rs index ccc89047c..501d7dc47 100644 --- a/tests/bootstrap.rs +++ b/tests/bootstrap.rs @@ -88,7 +88,7 @@ proptest! { } /// `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()) { diff --git a/tests/bootstrap_snapshot.rs b/tests/bootstrap_snapshot.rs index b0c65d995..04429500f 100644 --- a/tests/bootstrap_snapshot.rs +++ b/tests/bootstrap_snapshot.rs @@ -118,10 +118,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/common/oracle.rs b/tests/common/oracle.rs index 474428453..507032e56 100644 --- a/tests/common/oracle.rs +++ b/tests/common/oracle.rs @@ -61,9 +61,11 @@ impl Oracle { } /// 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). +/// 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() } diff --git a/tests/common/schedule/events.rs b/tests/common/schedule/events.rs index 118b9aca0..43a77e750 100644 --- a/tests/common/schedule/events.rs +++ b/tests/common/schedule/events.rs @@ -13,8 +13,10 @@ pub enum Event { }, /// 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. + /// 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/multi_peer.rs b/tests/multi_peer.rs index 16b3b2d44..54c13a591 100644 --- a/tests/multi_peer.rs +++ b/tests/multi_peer.rs @@ -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 a9b1463c5..90b81d0c5 100644 --- a/tests/opening_supply.rs +++ b/tests/opening_supply.rs @@ -25,8 +25,9 @@ fn seeded() -> Rumors { Peer::seed_rng(&mut SmallRng::seed_from_u64(0)).into_rumors() } -/// Pool size for the one-byte path search staging the disputed sibling: -/// the search must hit one *specific* root radix, a direct-hit search with +/// Pool size for the one-byte path search staging the disputed sibling. +/// +/// 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). From 2fb0639c91c323bda2adba0f64636684d50b391e Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 21:49:08 -0400 Subject: [PATCH 06/11] tests: split an over-long capture-test doc summary The doclint summary cap admits one short first paragraph; the renderer pin's statement moves below the fold. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- src/tree/mirror/streaming/remote/codec/capture/tests.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/tree/mirror/streaming/remote/codec/capture/tests.rs b/src/tree/mirror/streaming/remote/codec/capture/tests.rs index bff292e1b..794af8e66 100644 --- a/src/tree/mirror/streaming/remote/codec/capture/tests.rs +++ b/src/tree/mirror/streaming/remote/codec/capture/tests.rs @@ -30,10 +30,11 @@ fn encode_listing(children: &[(u8, Hash)]) -> Vec { body } -/// The capture renderer decodes each supply record structurally: two runs -/// differing only in the record's version render record lines that differ -/// exactly at the named version field, with the record count and the -/// (identical) payload accounting unchanged. +/// The capture renderer decodes each supply record structurally. +/// +/// Two runs differing only in the record's version render record lines +/// that differ exactly at the named version field, with the record count +/// and the (identical) payload accounting unchanged. #[test] fn supply_decode_names_the_field_that_moved() { let party = before::Party::seed(); From 51d95d8d5d490eb7d00762fa5ee3af53b271fedc Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 19 Aug 2026 08:34:58 -0400 Subject: [PATCH 07/11] gate: clear clippy and doc-shape findings in the migrated tests An op_ref in the apply walk's identity check, a Path conversion left over from the retired key type, two helper functions inserted between tests and their doc comments, one orphaned doc block folded into the pin it described, and the V1 wire codec gated to its consumers (the alternating protocol and the typed tree's tests), so default builds carry no dead codec. --- src/tree.rs | 3 +++ .../streaming/remote/codec/capture/tests.rs | 12 +++--------- .../mirror/streaming/remote/proxy/start/tests.rs | 15 +++++++-------- src/tree/tests.rs | 2 +- src/tree/traverse/act.rs | 2 +- src/tree/typed/node.rs | 4 ++++ src/tree/typed/prefix.rs | 3 +++ src/tree/typed/untyped.rs | 1 + 8 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/tree.rs b/src/tree.rs index 36ddbb6f7..275241686 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -64,6 +64,9 @@ use std::sync::Arc; 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}; diff --git a/src/tree/mirror/streaming/remote/codec/capture/tests.rs b/src/tree/mirror/streaming/remote/codec/capture/tests.rs index 794af8e66..35b2fb32b 100644 --- a/src/tree/mirror/streaming/remote/codec/capture/tests.rs +++ b/src/tree/mirror/streaming/remote/codec/capture/tests.rs @@ -13,13 +13,6 @@ 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. -/// -/// That one-line diff is the field-level account an insta re-accept -/// shows beside the hex. - /// Encode a listing as its wire form: raw radix-hash records. fn encode_listing(children: &[(u8, Hash)]) -> Vec { let mut body = Vec::new(); @@ -33,8 +26,9 @@ fn encode_listing(children: &[(u8, Hash)]) -> Vec { /// The capture renderer decodes each supply record structurally. /// /// Two runs differing only in the record's version render record lines -/// that differ exactly at the named version field, with the record count -/// and the (identical) payload accounting unchanged. +/// 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(); diff --git a/src/tree/mirror/streaming/remote/proxy/start/tests.rs b/src/tree/mirror/streaming/remote/proxy/start/tests.rs index 7fbe5fb35..8e3e0211e 100644 --- a/src/tree/mirror/streaming/remote/proxy/start/tests.rs +++ b/src/tree/mirror/streaming/remote/proxy/start/tests.rs @@ -62,14 +62,6 @@ fn ticked_version() -> Version { version } -/// A greeting cut inside the version frame's length header fails as a typed -/// read error. -/// -/// The four header bytes are the first peer-controlled bytes of the -/// greeting; a peer that closes mid-header must surface -/// [`Error::HandshakeRead`] with `UnexpectedEof` — never a hang waiting on -/// bytes that cannot arrive. - /// 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 { @@ -81,6 +73,13 @@ fn encode_listing(children: &[(u8, Hash)]) -> Vec { body } +/// A greeting cut inside the version frame's length header fails as a typed +/// read error. +/// +/// The four header bytes are the first peer-controlled bytes of the +/// greeting; a peer that closes mid-header must surface +/// [`Error::HandshakeRead`] with `UnexpectedEof` — never a hang waiting on +/// bytes that cannot arrive. #[pollster::test] async fn truncated_version_header_is_a_typed_read_error() { let result = receive_greeting(&[0, 0]).await.map(|_| ()); diff --git a/src/tree/tests.rs b/src/tree/tests.rs index a999848e5..d752cd8f5 100644 --- a/src/tree/tests.rs +++ b/src/tree/tests.rs @@ -259,7 +259,7 @@ proptest! { let mut version = Version::new(); version.tick(&crate::tree::arb::nth_party(index)); let message = msg(b.clone()); - let key = Path::for_leaf(&version).into(); + let key = Path::for_leaf(&version); (key, version, message) }; let index_of: HashMap = kept diff --git a/src/tree/traverse/act.rs b/src/tree/traverse/act.rs index 0a4716776..c5e6e96cd 100644 --- a/src/tree/traverse/act.rs +++ b/src/tree/traverse/act.rs @@ -188,7 +188,7 @@ impl Act for Z { // an off-model hash collision (see `LeafCollision`), and // errored before anything commits. if let (Action::Insert(value), Some(existing)) = (&action, &node) { - if existing.ceiling() != &version + if *existing.ceiling() != version || existing.message().as_slice() != value.as_slice() { return Err(LeafCollision { diff --git a/src/tree/typed/node.rs b/src/tree/typed/node.rs index 32e7be999..2070c7ccf 100644 --- a/src/tree/typed/node.rs +++ b/src/tree/typed/node.rs @@ -10,6 +10,7 @@ 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}; @@ -459,6 +460,7 @@ 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. +#[cfg(any(test, feature = "protocol-v1"))] impl wire::Encode for Node where H: Height, @@ -468,6 +470,7 @@ where } } +#[cfg(any(test, feature = "protocol-v1"))] impl wire::Decode for Node where T: serde::de::DeserializeOwned, @@ -483,6 +486,7 @@ where } } +#[cfg(any(test, feature = "protocol-v1"))] impl wire::Decode for Node> where T: serde::de::DeserializeOwned, diff --git a/src/tree/typed/prefix.rs b/src/tree/typed/prefix.rs index 2d1ff51fc..ab7911925 100644 --- a/src/tree/typed/prefix.rs +++ b/src/tree/typed/prefix.rs @@ -2,6 +2,7 @@ use std::{fmt::Debug, marker::PhantomData}; use tinyvec::ArrayVec; +#[cfg(any(test, feature = "protocol-v1"))] use crate::tree::wire; use super::height::{Height, Root, S, Z}; @@ -157,6 +158,7 @@ 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. +#[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; @@ -171,6 +173,7 @@ impl wire::Encode for Prefix { } } +#[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; diff --git a/src/tree/typed/untyped.rs b/src/tree/typed/untyped.rs index 730809765..562692316 100644 --- a/src/tree/typed/untyped.rs +++ b/src/tree/typed/untyped.rs @@ -675,6 +675,7 @@ 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. + #[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()) From 07fb5c3fa63e74251ba1042a375590b2e5bc3dce Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 19 Aug 2026 11:11:14 -0400 Subject: [PATCH 08/11] gate: handle act's Result in the merged supply declarations tests The opening_bulk_pair helper landed on main after this branch's act became fallible; the rebase merged it textually while clippy's denied unused-Result caught the semantic seam. Handled with the collision-free expectation every other test call site states. --- .../remote/proxy/tests/declarations.rs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/tree/mirror/streaming/remote/proxy/tests/declarations.rs b/src/tree/mirror/streaming/remote/proxy/tests/declarations.rs index ad1c7cc9c..c56cde7ff 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests/declarations.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests/declarations.rs @@ -284,15 +284,19 @@ fn understated_set_len_fails_the_session() { /// exclusive content rides the opening-supply stream as one reply. fn opening_bulk_pair() -> (crate::tree::Root<()>, crate::tree::Root<()>) { let mut small = Tree::new(); - small.act( - &nth_party(1), - (0..4).map(|_| Action::Insert(Message::new(()))), - ); + small + .act( + &nth_party(1), + (0..4).map(|_| Action::Insert(Message::new(()))), + ) + .expect("collision-free by construction"); let mut large = Tree::new(); - large.act( - &nth_party(0), - (0..8).map(|_| Action::Insert(Message::new(()))), - ); + large + .act( + &nth_party(0), + (0..8).map(|_| Action::Insert(Message::new(()))), + ) + .expect("collision-free by construction"); (small.root, large.root) } From c6fe401878d6fb1dabf969c5347589a760f58205 Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 19 Aug 2026 11:34:32 -0400 Subject: [PATCH 09/11] style: import serde trait names instead of qualifying them inline A mechanized sweep: serde::de::DeserializeOwned, serde::Serialize, and the serializer traits move into use lines at each consuming file (two imports cfg-gated to their exclusively gated use sites). The qualified trait-method calls on serde::de::Error and serde::ser::Error stay: a bare Error import would collide with the local error types. --- src/batch.rs | 3 ++- src/message.rs | 25 +++++++++++-------- src/message/tests.rs | 5 ++-- src/network.rs | 12 ++++++--- src/peer.rs | 6 +++-- src/peer/bootstrap.rs | 6 +++-- src/peer/gossip.rs | 14 ++++++----- src/rumors.rs | 8 +++--- src/tree/mirror/alternating/backend/remote.rs | 16 ++++++------ src/tree/mirror/alternating/message.rs | 7 +++--- src/tree/mirror/alternating/tests.rs | 4 ++- .../mirror/streaming/remote/adapter/decode.rs | 13 +++++----- .../mirror/streaming/remote/codec/decode.rs | 14 +++++------ .../streaming/remote/codec/decode/async_io.rs | 5 ++-- .../mirror/streaming/remote/codec/frame.rs | 5 ++-- .../mirror/streaming/remote/codec/tests.rs | 3 ++- .../remote/codec/tests/error_atlas.rs | 3 ++- .../mirror/streaming/remote/proxy/start.rs | 11 ++++---- .../mirror/streaming/remote/proxy/state.rs | 19 +++++++------- .../mirror/streaming/remote/proxy/tests.rs | 7 +++--- .../mirror/streaming/remote/proxy/work.rs | 3 ++- .../streaming/remote/proxy/work/pump.rs | 7 +++--- src/tree/mirror/streaming/remote/streams.rs | 7 +++--- src/tree/mirror/streaming/tests/fixtures.rs | 5 ++-- src/tree/tests.rs | 3 ++- src/tree/typed/node.rs | 7 ++++-- src/tree/wire.rs | 3 ++- tests/bootstrap.rs | 4 ++- tests/bootstrap_snapshot.rs | 4 ++- tests/cbor_evolution.rs | 5 ++-- tests/common/action.rs | 4 ++- tests/common/gossip_snapshot.rs | 6 +++-- tests/common/overlap.rs | 6 +++-- tests/common/peer.rs | 12 +++++---- tests/common/schedule/executor.rs | 14 ++++++----- tests/common/wire.rs | 20 ++++++++------- tests/dispute_wire.rs | 8 +++--- tests/hop_trace.rs | 4 ++- tests/pairwise.rs | 4 ++- tests/session_stats.rs | 4 ++- tests/single_peer.rs | 6 +++-- tests/tradeoff_probe.rs | 8 +++--- 42 files changed, 200 insertions(+), 130 deletions(-) diff --git a/src/batch.rs b/src/batch.rs index e2b4b8322..1b6c52aac 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -5,6 +5,7 @@ use crate::tree::Action; 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. /// @@ -62,7 +63,7 @@ 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: serde::Serialize, + T: Serialize, { self.actions.push(Action::Insert(Message::from(message))); self diff --git a/src/message.rs b/src/message.rs index c13538bf0..dd8779615 100644 --- a/src/message.rs +++ b/src/message.rs @@ -6,6 +6,11 @@ use std::sync::Arc; 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` @@ -61,7 +66,7 @@ fn de_error(error: ciborium::de::Error) -> io::Error { /// 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 { +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)"); @@ -77,7 +82,7 @@ impl Message { /// If the message cannot be serialized (see [`Message`]). pub fn new(message: T) -> Self where - T: serde::Serialize, + T: Serialize, { Message { serialized: Bytes::from(to_vec(&message)), @@ -93,7 +98,7 @@ impl Message { /// encoding. pub fn from_slice(bytes: &[u8]) -> io::Result where - T: serde::de::DeserializeOwned, + T: DeserializeOwned, { let mut input = bytes; let message: T = ciborium::de::from_reader(&mut input).map_err(de_error)?; @@ -130,7 +135,7 @@ impl Message { /// [`from_slice`](Self::from_slice)'s exactly-one-value contract. pub fn from_bytes(bytes: Bytes) -> io::Result where - T: serde::de::DeserializeOwned, + T: DeserializeOwned, { let mut input = bytes.as_ref(); let message: T = ciborium::de::from_reader(&mut input).map_err(de_error)?; @@ -153,7 +158,7 @@ impl Message { /// If the message cannot be serialized (see [`Message`]). pub fn from_arc(arc: Arc) -> Self where - T: serde::Serialize, + T: Serialize, { Message { serialized: Bytes::from(to_vec(&*arc)), @@ -212,7 +217,7 @@ impl Message { } } -impl From for Message { +impl From for Message { /// Creates a `Message` pairing the given object with its cached /// serialization. /// @@ -278,14 +283,14 @@ impl Hash for Message { // wrapper is what makes a nested message self-delimiting wherever the // container does not delimit it. -impl serde::Serialize for Message { - fn serialize(&self, serializer: S) -> Result { +impl Serialize for Message { + fn serialize(&self, serializer: S) -> Result { serializer.serialize_bytes(&self.serialized) } } -impl<'de, T: serde::de::DeserializeOwned> serde::Deserialize<'de> for Message { - fn deserialize>(deserializer: D) -> Result { +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 218ca96f4..5d8451960 100644 --- a/src/message/tests.rs +++ b/src/message/tests.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use super::Message; +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. @@ -86,8 +87,8 @@ proptest! { #[test] fn serde_form_wraps_cached_bytes(p in payload()) { struct Bstr<'a>(&'a [u8]); - impl serde::Serialize for Bstr<'_> { - fn serialize(&self, s: S) -> Result { + impl Serialize for Bstr<'_> { + fn serialize(&self, s: S) -> Result { s.serialize_bytes(self.0) } } diff --git a/src/network.rs b/src/network.rs index c96f49986..dfa592ea0 100644 --- a/src/network.rs +++ b/src/network.rs @@ -4,6 +4,10 @@ use std::fmt; 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). /// @@ -24,14 +28,14 @@ pub struct Network([u8; 16]); // opaque, so no structure beyond its width belongs on the wire or on disk // (the bookmark record keys its map by it). -impl serde::Serialize for Network { - fn serialize(&self, serializer: S) -> Result { +impl Serialize for Network { + fn serialize(&self, serializer: S) -> Result { serializer.serialize_bytes(&self.0) } } -impl<'de> serde::Deserialize<'de> for Network { - fn deserialize>(deserializer: D) -> Result { +impl<'de> Deserialize<'de> for Network { + fn deserialize>(deserializer: D) -> Result { let bytes = >::deserialize(deserializer)?; let bytes: [u8; 16] = bytes .as_slice() diff --git a/src/peer.rs b/src/peer.rs index b1923e747..ed0e2b2ab 100644 --- a/src/peer.rs +++ b/src/peer.rs @@ -21,6 +21,8 @@ use crate::{ Version, }; +use serde::Serialize; +use serde::de::DeserializeOwned; mod bootstrap; mod gossip; @@ -273,7 +275,7 @@ impl Peer { /// promises](crate::link::Link#what-a-session-promises). pub async fn retire(self, link: &mut Link) -> Retire where - T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -542,7 +544,7 @@ impl Peer { pub(crate) fn send(&self, message: T) -> Batch<'_, T> where - T: serde::Serialize + Send + Sync, + T: Serialize + Send + Sync, { let mut batch = self.batch(); batch.send(message); diff --git a/src/peer/bootstrap.rs b/src/peer/bootstrap.rs index 120a5e2cc..46f33746c 100644 --- a/src/peer/bootstrap.rs +++ b/src/peer/bootstrap.rs @@ -14,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`]. /// @@ -215,7 +217,7 @@ impl Bootstrap { link: &mut Link, ) -> Result>, Error> where - T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -295,7 +297,7 @@ impl BookmarkedBootstrap { /// in every outcome that never used it. pub async fn join(self, link: &mut Link) -> Joined where - T: serde::de::DeserializeOwned + serde::Serialize + 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 68aae1024..b1331bc45 100644 --- a/src/peer/gossip.rs +++ b/src/peer/gossip.rs @@ -41,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"; @@ -209,7 +211,7 @@ impl Peer { link: &'a mut Link, ) -> BoxFuture<'a, Result, Error>> where - T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -236,7 +238,7 @@ impl Peer { link: DynLinkParts<'a>, ) -> BoxFuture<'a, Result, Error>> where - T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, { Box::pin(async move { let (read, write, connector, acceptor, epoch) = link; @@ -435,7 +437,7 @@ impl Peer { link: &mut Link, ) -> Retire where - T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -474,7 +476,7 @@ impl Peer { link: &mut Link, ) -> Result> where - T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -614,7 +616,7 @@ impl Peer { link: DynLinkParts<'a>, ) -> (Intent, Result<(Version, SessionStats), Error>) where - T: serde::de::DeserializeOwned + serde::Serialize + 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 @@ -969,7 +971,7 @@ impl Peer { link: &'a mut Link, ) -> impl Stream>> + Unpin + 'a where - T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, diff --git a/src/rumors.rs b/src/rumors.rs index 39df14e91..a2a4fabea 100644 --- a/src/rumors.rs +++ b/src/rumors.rs @@ -17,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. /// @@ -160,7 +162,7 @@ impl Rumors { /// If `message` fails to serialize (see [`Batch::send`]). pub fn send(&self, message: T) -> Batch<'_, T> where - T: serde::Serialize + Send + Sync, + T: Serialize + Send + Sync, { self.peer.send(message) } @@ -391,7 +393,7 @@ impl Rumors { link: &mut Link, ) -> Result> where - T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, @@ -507,7 +509,7 @@ impl Rumors { link: &'a mut Link, ) -> impl Stream>> + Unpin + 'a where - T: serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static, + T: DeserializeOwned + Serialize + Send + Sync + 'static, CR: AsyncRead + Unpin + Send, CW: AsyncWrite + Unpin + Send, C: Connector, diff --git a/src/tree/mirror/alternating/backend/remote.rs b/src/tree/mirror/alternating/backend/remote.rs index 8fb5b9eb8..a54219cb2 100644 --- a/src/tree/mirror/alternating/backend/remote.rs +++ b/src/tree/mirror/alternating/backend/remote.rs @@ -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; @@ -170,7 +172,7 @@ impl protocol::Accept for Exchange where R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, - T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync, + T: Serialize + DeserializeOwned + Send + Sync, { type Next = Exchange; @@ -208,7 +210,7 @@ where impl protocol::Initiator for Exchange where - T: serde::de::DeserializeOwned + Send + Sync, + T: DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, Node: wire::Decode, @@ -230,7 +232,7 @@ where impl protocol::Responder for Exchange where - T: serde::de::DeserializeOwned + Send + Sync, + T: DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, Node: wire::Decode, @@ -258,7 +260,7 @@ where impl protocol::OpenInitiator for Exchange where - T: serde::de::DeserializeOwned + Send + Sync, + T: DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, Node: wire::Decode, @@ -292,7 +294,7 @@ where impl protocol::Exchange for Exchange>> where - T: serde::de::DeserializeOwned + Send + Sync, + T: DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, H: Height, @@ -341,7 +343,7 @@ where impl protocol::CloseResponder for Exchange> where - T: serde::de::DeserializeOwned + Send + Sync, + T: DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, { @@ -382,7 +384,7 @@ where impl protocol::CompleteInitiator for Exchange where - T: serde::de::DeserializeOwned + Send + Sync, + T: DeserializeOwned + Send + Sync, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, { diff --git a/src/tree/mirror/alternating/message.rs b/src/tree/mirror/alternating/message.rs index 9d53a24db..8b05e3fe0 100644 --- a/src/tree/mirror/alternating/message.rs +++ b/src/tree/mirror/alternating/message.rs @@ -80,6 +80,7 @@ use crate::tree::typed::{ height::{Height, Root, S, UnderRoot, Z}, }; +use serde::de::DeserializeOwned; #[cfg(test)] mod tests; @@ -225,7 +226,7 @@ where // discharges it. impl Decode for Exchange where - T: serde::de::DeserializeOwned, + T: DeserializeOwned, S: Height, H: Height, Node>: Decode, @@ -309,7 +310,7 @@ impl Encode for Closing { impl Decode for Closing where - T: serde::de::DeserializeOwned, + T: DeserializeOwned, { fn read_wire(reader: &mut R) -> std::io::Result { let providing: Providing = Decode::read_wire(reader)?; @@ -355,7 +356,7 @@ impl Encode for Complete { impl Decode for Complete where - T: serde::de::DeserializeOwned, + T: DeserializeOwned, { fn read_wire(reader: &mut R) -> std::io::Result { let providing: Providing = Decode::read_wire(reader)?; diff --git a/src/tree/mirror/alternating/tests.rs b/src/tree/mirror/alternating/tests.rs index 9e70f8885..6441ac34d 100644 --- a/src/tree/mirror/alternating/tests.rs +++ b/src/tree/mirror/alternating/tests.rs @@ -20,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 @@ -82,7 +84,7 @@ fn mirror_via( scenario: Scenario, ) -> crate::tree::Root where - T: PartialEq + std::fmt::Debug + serde::Serialize + serde::de::DeserializeOwned + Send + Sync, + T: PartialEq + std::fmt::Debug + Serialize + DeserializeOwned + Send + Sync, { block_on(async move { match scenario { diff --git a/src/tree/mirror/streaming/remote/adapter/decode.rs b/src/tree/mirror/streaming/remote/adapter/decode.rs index 660fbc966..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 @@ -85,7 +86,7 @@ pub fn early_supplies( ) -> impl Stream), DecodeError>> + Send where B: Backend: Leaf>, - T: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, G: Height, S: Height, F: Stream> + Unpin, @@ -219,7 +220,7 @@ pub async fn decode_reply( ) -> Result, Vec>>, DecodeError> where B: Backend: Leaf>, - T: serde::de::DeserializeOwned + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, H: Height, S: Convert, S>: Height, @@ -249,7 +250,7 @@ pub async fn decode_leaf_reply( ) -> Result>>, DecodeError> where B: Backend: Leaf>, - T: serde::de::DeserializeOwned + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, F: Stream> + Unpin, { decode( @@ -279,7 +280,7 @@ async fn decode( ) -> Result>, DecodeError> where B: Backend: Leaf>, - T: serde::de::DeserializeOwned + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, H: Convert, S: Height, F: Stream> + Unpin, @@ -323,7 +324,7 @@ async fn read_reply( ) -> Result>, DecodeError> where B: Backend: Leaf>, - T: serde::de::DeserializeOwned + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, H: Height, S: Height, F: Stream> + Unpin, diff --git a/src/tree/mirror/streaming/remote/codec/decode.rs b/src/tree/mirror/streaming/remote/codec/decode.rs index 3530a447a..f6ae3ea1e 100644 --- a/src/tree/mirror/streaming/remote/codec/decode.rs +++ b/src/tree/mirror/streaming/remote/codec/decode.rs @@ -26,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, @@ -38,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], @@ -75,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) @@ -90,10 +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 7914a5fd2..4a8866228 100644 --- a/src/tree/mirror/streaming/remote/codec/decode/async_io.rs +++ b/src/tree/mirror/streaming/remote/codec/decode/async_io.rs @@ -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> { diff --git a/src/tree/mirror/streaming/remote/codec/frame.rs b/src/tree/mirror/streaming/remote/codec/frame.rs index da60c1495..712f6c46c 100644 --- a/src/tree/mirror/streaming/remote/codec/frame.rs +++ b/src/tree/mirror/streaming/remote/codec/frame.rs @@ -14,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; @@ -207,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: serde::de::DeserializeOwned, + T: DeserializeOwned, { self.record_slices().map(parse_record) } @@ -262,7 +263,7 @@ 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> { // Both fields are self-delimiting CBOR values, so the exact record diff --git a/src/tree/mirror/streaming/remote/codec/tests.rs b/src/tree/mirror/streaming/remote/codec/tests.rs index a6af68d68..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 7484f4b8e..2929a65c0 100644 --- a/src/tree/mirror/streaming/remote/codec/tests/error_atlas.rs +++ b/src/tree/mirror/streaming/remote/codec/tests/error_atlas.rs @@ -30,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 @@ -81,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"); diff --git a/src/tree/mirror/streaming/remote/proxy/start.rs b/src/tree/mirror/streaming/remote/proxy/start.rs index 9ad0282da..ec4868000 100644 --- a/src/tree/mirror/streaming/remote/proxy/start.rs +++ b/src/tree/mirror/streaming/remote/proxy/start.rs @@ -35,6 +35,7 @@ use crate::{ }, }; +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: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, R: AsyncRead + Unpin + Send, W: AsyncWrite + Unpin + Send, C: Connector, @@ -322,7 +323,7 @@ fn connected( ) -> Connected where B: Backend: Leaf>, - T: serde::de::DeserializeOwned + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, C: Connector, A: Acceptor, { @@ -377,7 +378,7 @@ fn open( ) -> Connected where B: Backend: Leaf>, - T: serde::de::DeserializeOwned + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, C: Connector, A: Acceptor, { diff --git a/src/tree/mirror/streaming/remote/proxy/state.rs b/src/tree/mirror/streaming/remote/proxy/state.rs index d550db9f7..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: serde::de::DeserializeOwned + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, C: Connector, A: Acceptor, { @@ -109,7 +110,7 @@ where impl Connected where B: Backend: Leaf>, - T: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + 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 37679cd44..60f8e9743 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: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + 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); diff --git a/src/tree/mirror/streaming/remote/proxy/work.rs b/src/tree/mirror/streaming/remote/proxy/work.rs index faad5c55d..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: serde::de::DeserializeOwned + 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 4590e5486..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: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + 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: serde::de::DeserializeOwned + 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 17e84fdaf..6d88a50b3 100644 --- a/src/tree/mirror/streaming/remote/streams.rs +++ b/src/tree/mirror/streaming/remote/streams.rs @@ -54,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 @@ -311,7 +312,7 @@ struct ReceiverStart { impl StreamReceiver where Rx: tokio::io::AsyncRead + Unpin + Send + 'static, - T: serde::de::DeserializeOwned + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, { /// Bind one incoming logical stream to its claim slot. pub fn new( @@ -382,7 +383,7 @@ pub enum ReceiverFinish { impl futures::Stream for StreamReceiver where Rx: tokio::io::AsyncRead + Unpin + Send + 'static, - T: serde::de::DeserializeOwned + Send + Sync + 'static, + T: DeserializeOwned + Send + Sync + 'static, { type Item = Frame; @@ -405,7 +406,7 @@ fn read_frames( ) -> impl futures::Stream> + Send where Rx: tokio::io::AsyncRead + Unpin + Send + 'static, - T: serde::de::DeserializeOwned + 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 507e41cec..26bc0cb31 100644 --- a/src/tree/mirror/streaming/tests/fixtures.rs +++ b/src/tree/mirror/streaming/tests/fixtures.rs @@ -14,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]; @@ -38,7 +39,7 @@ pub(super) fn grown( paths: &[Path], ) -> Option> where - T: serde::Serialize + Clone + Send + Sync, + T: Serialize + Clone + Send + Sync, { assert!(stride > 0, "each leaf needs a fresh version"); let party = nth_party(party); @@ -237,7 +238,7 @@ impl Divergence { /// in both. pub fn trees(&self, value: &T) -> (Root, Root, Root) where - T: serde::Serialize + 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/tests.rs b/src/tree/tests.rs index d752cd8f5..80e2bc71b 100644 --- a/src/tree/tests.rs +++ b/src/tree/tests.rs @@ -7,6 +7,7 @@ use super::typed::{Hash, Path, untyped}; use super::*; use crate::message::Message; +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) @@ -1625,7 +1626,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, serde::Serialize)] +#[derive(Debug, Serialize)] struct DropBomb { armed: bool, } diff --git a/src/tree/typed/node.rs b/src/tree/typed/node.rs index 2070c7ccf..81e0eff3e 100644 --- a/src/tree/typed/node.rs +++ b/src/tree/typed/node.rs @@ -14,6 +14,9 @@ use super::untyped; 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; @@ -473,7 +476,7 @@ where #[cfg(any(test, feature = "protocol-v1"))] impl wire::Decode for Node where - T: serde::de::DeserializeOwned, + T: DeserializeOwned, { fn read_wire(reader: &mut R) -> std::io::Result { let prefix_len = u8::read_wire(reader)?; @@ -489,7 +492,7 @@ where #[cfg(any(test, feature = "protocol-v1"))] impl wire::Decode for Node> where - T: serde::de::DeserializeOwned, + T: DeserializeOwned, H: Height, S: Height, Node: wire::Decode, diff --git a/src/tree/wire.rs b/src/tree/wire.rs index b59920c70..4a420690f 100644 --- a/src/tree/wire.rs +++ b/src/tree/wire.rs @@ -21,6 +21,7 @@ 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 @@ -213,7 +214,7 @@ impl Encode for Message { } } -impl Decode for Message { +impl Decode for Message { fn read_wire(reader: &mut R) -> std::io::Result { ciborium::de::from_reader(reader).map_err(de_error) } diff --git a/tests/bootstrap.rs b/tests/bootstrap.rs index 501d7dc47..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: serde::Serialize + serde::de::DeserializeOwned + 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); diff --git a/tests/bootstrap_snapshot.rs b/tests/bootstrap_snapshot.rs index 04429500f..1e9b93c89 100644 --- a/tests/bootstrap_snapshot.rs +++ b/tests/bootstrap_snapshot.rs @@ -33,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. @@ -49,7 +51,7 @@ fn seeded() -> Rumors { /// expected to be served a successor. fn capture_bootstrap(provider: Rumors) -> String where - T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { capture_session( move |mut link| async move { diff --git a/tests/cbor_evolution.rs b/tests/cbor_evolution.rs index 617f2ad83..90a4daa28 100644 --- a/tests/cbor_evolution.rs +++ b/tests/cbor_evolution.rs @@ -17,6 +17,7 @@ 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 { @@ -55,8 +56,8 @@ enum EventV2 { /// across two payload *types*. async fn exchanged(payload: A) -> B where - A: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, - B: serde::Serialize + serde::de::DeserializeOwned + Clone + Send + Sync + 'static, + A: Serialize + DeserializeOwned + Send + Sync + 'static, + B: Serialize + DeserializeOwned + Clone + Send + Sync + 'static, { let sender = Peer::::seed().into_rumors(); sender.send(payload); diff --git a/tests/common/action.rs b/tests/common/action.rs index 61ef8dd02..63cabf7aa 100644 --- a/tests/common/action.rs +++ b/tests/common/action.rs @@ -4,6 +4,8 @@ use proptest::collection::vec; use proptest::prelude::*; use rumors::{Snapshot, Version, causally}; +use serde::Serialize; +use serde::de::DeserializeOwned; const MAX_ACTIONS: usize = 16; #[derive(Debug, Clone)] @@ -64,7 +66,7 @@ pub fn minted_version(snapshot: &Snapshot, pre: &Version) -> /// 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 + serde::Serialize + serde::de::DeserializeOwned + 'static, + T: Send + Sync + Clone + Serialize + DeserializeOwned + 'static, { let mut versions: Vec = Vec::new(); for a in actions { diff --git a/tests/common/gossip_snapshot.rs b/tests/common/gossip_snapshot.rs index 6d2cd9c53..20233a5bf 100644 --- a/tests/common/gossip_snapshot.rs +++ b/tests/common/gossip_snapshot.rs @@ -50,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)] @@ -363,7 +365,7 @@ where /// reconcile cleanly; a gossip error panics the helper. pub fn capture_gossip(a: Rumors, b: Rumors) -> String where - T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { capture_session( move |mut link| async move { @@ -378,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { capture_session_v1( move |mut link| async move { diff --git a/tests/common/overlap.rs b/tests/common/overlap.rs index 458506113..ed94ec556 100644 --- a/tests/common/overlap.rs +++ b/tests/common/overlap.rs @@ -41,6 +41,8 @@ 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): @@ -90,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { let a = a.clone(); let b = b.clone(); @@ -199,7 +201,7 @@ pub struct OverlapSchedule { /// two agree. pub fn execute_overlap_and_quiesce(schedule: &OverlapSchedule) -> (Vec>, Oracle) where - T: Clone + Eq + Ord + serde::Serialize + serde::de::DeserializeOwned + 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 { diff --git a/tests/common/peer.rs b/tests/common/peer.rs index 6f80c1888..47517c7a7 100644 --- a/tests/common/peer.rs +++ b/tests/common/peer.rs @@ -17,6 +17,8 @@ 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, @@ -34,7 +36,7 @@ pub struct Peer { 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. @@ -97,7 +99,7 @@ impl(a: &mut Peer, b: &mut Peer) where - T: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Clone + Serialize + DeserializeOwned + Send + Sync + 'static, { block_on(wire_gossip_async(&a.local, &b.local)); a.drain(); @@ -110,7 +112,7 @@ where /// non-termination guard. pub fn quiesce(peers: &mut [Peer]) where - T: Clone + Eq + serde::Serialize + serde::de::DeserializeOwned + 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); @@ -120,7 +122,7 @@ where /// slotted fleet, skipping retired peers' vacated slots. pub fn quiesce_slots(slots: &mut [Option>]) where - T: Clone + Eq + serde::Serialize + serde::de::DeserializeOwned + 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); @@ -137,7 +139,7 @@ where /// should catch). fn quiesce_refs(peers: &mut [&mut Peer]) where - T: Clone + Eq + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Clone + Eq + Serialize + DeserializeOwned + Send + Sync + 'static, { let n = peers.len(); if n < 2 { diff --git a/tests/common/schedule/executor.rs b/tests/common/schedule/executor.rs index 951ed37ec..393f891fd 100644 --- a/tests/common/schedule/executor.rs +++ b/tests/common/schedule/executor.rs @@ -19,6 +19,8 @@ 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, @@ -63,7 +65,7 @@ impl MembershipExecutionResult { /// between differently-configured endpoints). pub fn execute(schedule: &Schedule, windows: &WindowAssignment) -> ExecutionResult where - T: Clone + Ord + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Clone + Ord + Serialize + DeserializeOwned + Send + Sync + 'static, { execute_with(schedule, windows, |_, _, _| true) } @@ -76,7 +78,7 @@ pub fn execute_and_quiesce( windows: &WindowAssignment, ) -> ExecutionResult where - T: Clone + Eq + Ord + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Clone + Eq + Ord + Serialize + DeserializeOwned + Send + Sync + 'static, { let mut result = execute(schedule, windows); quiesce(&mut result.peers); @@ -110,7 +112,7 @@ pub fn execute_with( allow_gossip: F, ) -> ExecutionResult where - T: Clone + Ord + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Clone + Ord + Serialize + DeserializeOwned + Send + Sync + 'static, F: Fn(usize, usize, EventIdx) -> bool, { assert!( @@ -139,7 +141,7 @@ pub fn execute_membership( windows: &WindowAssignment, ) -> MembershipExecutionResult where - T: Clone + Ord + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Clone + Ord + Serialize + DeserializeOwned + Send + Sync + 'static, { execute_slots(schedule, windows, |_, _, _| true) } @@ -151,7 +153,7 @@ pub fn execute_membership_and_quiesce( windows: &WindowAssignment, ) -> MembershipExecutionResult where - T: Clone + Eq + Ord + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Clone + Eq + Ord + Serialize + DeserializeOwned + Send + Sync + 'static, { let mut result = execute_membership(schedule, windows); quiesce_slots(&mut result.slots); @@ -176,7 +178,7 @@ fn execute_slots( allow_gossip: F, ) -> MembershipExecutionResult where - T: Clone + Ord + serde::Serialize + serde::de::DeserializeOwned + 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); diff --git a/tests/common/wire.rs b/tests/common/wire.rs index e617eb4c9..3e4df5e99 100644 --- a/tests/common/wire.rs +++ b/tests/common/wire.rs @@ -17,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 @@ -120,7 +122,7 @@ fn unread_control_bytes(mut read: R) -> Vec { #[track_caller] pub fn wire_gossip(a: &Rumors, b: &Rumors) where - T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { block_on(wire_gossip_async(a, b)); } @@ -129,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { let _ = gossip_pair_async(a, b).await; } @@ -144,7 +146,7 @@ pub async fn gossip_pair_async( b: &Rumors, ) -> (rumors::Gossiped, rumors::Gossiped) where - T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { let (mut a_link, mut b_link) = rumors::link::memory_with_capacity(LINK_BUF); @@ -205,7 +207,7 @@ pub async fn divergent_pair( #[track_caller] pub fn bootstrap_fork(parent: &Rumors) -> Rumors where - T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { block_on(bootstrap_fork_async_with_protocol(parent, Protocol::V2)) } @@ -214,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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { bootstrap_fork_async_with_protocol(parent, Protocol::V2).await } @@ -230,7 +232,7 @@ pub async fn bootstrap_fork_async_with_protocol( protocol: Protocol, ) -> Rumors where - T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { bootstrap_fork_configured(parent, protocol, WindowChoice::Floor).await } @@ -240,7 +242,7 @@ where #[track_caller] pub fn bootstrap_fork_with_window(parent: &Rumors, window: WindowChoice) -> Rumors where - T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { block_on(bootstrap_fork_with_window_async(parent, window)) } @@ -252,7 +254,7 @@ pub async fn bootstrap_fork_with_window_async( window: WindowChoice, ) -> Rumors where - T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { bootstrap_fork_configured(parent, Protocol::V2, window).await } @@ -265,7 +267,7 @@ async fn bootstrap_fork_configured( window: WindowChoice, ) -> Rumors where - T: serde::Serialize + serde::de::DeserializeOwned + 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 ad7ad5557..1ebe3f46f 100644 --- a/tests/dispute_wire.rs +++ b/tests/dispute_wire.rs @@ -44,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; @@ -181,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: serde::Serialize + serde::de::DeserializeOwned + 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); @@ -203,7 +205,7 @@ where /// side of each end. fn session_wire_bytes(a: &Rumors, b: &Rumors) -> usize where - T: serde::Serialize + serde::de::DeserializeOwned + 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); @@ -226,7 +228,7 @@ where /// cost the constant states. fn implied_bytes_per_message(mint: impl FnMut(&mut SmallRng) -> T) -> usize where - T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { let (left, right) = diverged(mint); let total = session_wire_bytes(&left, &right); diff --git a/tests/hop_trace.rs b/tests/hop_trace.rs index 875e93094..6b498541d 100644 --- a/tests/hop_trace.rs +++ b/tests/hop_trace.rs @@ -47,6 +47,8 @@ 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: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Serialize + DeserializeOwned + Send + Sync + 'static, { let trace = Trace::default(); let (mut a_link, mut b_link) = traced_pair(&trace); diff --git a/tests/pairwise.rs b/tests/pairwise.rs index 94bb3ac75..846822c9b 100644 --- a/tests/pairwise.rs +++ b/tests/pairwise.rs @@ -25,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 + serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static, + T: Clone + Serialize + DeserializeOwned + Send + Sync + 'static, { bootstrap_fork(k) } diff --git a/tests/session_stats.rs b/tests/session_stats.rs index a10a4e02d..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: serde::Serialize + serde::de::DeserializeOwned + 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)); diff --git a/tests/single_peer.rs b/tests/single_peer.rs index 19a8e27d5..cee4af3a4 100644 --- a/tests/single_peer.rs +++ b/tests/single_peer.rs @@ -15,6 +15,8 @@ use rumors::{Peer, Rumors, Version, causally}; use crate::common::wire::block_on; +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 { @@ -153,8 +155,8 @@ struct Explosive { fail: bool, } -impl serde::Serialize for Explosive { - fn serialize(&self, serializer: S) -> Result { +impl Serialize for Explosive { + fn serialize(&self, serializer: S) -> Result { if self.fail { return Err(serde::ser::Error::custom("detonated")); } diff --git a/tests/tradeoff_probe.rs b/tests/tradeoff_probe.rs index 36e80892c..5af10104f 100644 --- a/tests/tradeoff_probe.rs +++ b/tests/tradeoff_probe.rs @@ -38,6 +38,8 @@ 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); @@ -57,7 +59,7 @@ const UNBOUNDED: usize = 8 << 30; fn diverged(budget: usize, mint: &mut impl FnMut(&mut SmallRng) -> T) -> (Rumors, Rumors) where - T: serde::Serialize + serde::de::DeserializeOwned + 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); @@ -99,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: serde::Serialize + serde::de::DeserializeOwned + 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() @@ -131,7 +133,7 @@ fn run_cells( targets: &[f64], mint: &mut impl FnMut(&mut SmallRng) -> T, ) where - T: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + Clone + 'static, + T: Serialize + DeserializeOwned + Send + Sync + Clone + 'static, { let (envelope, _) = envelope_and_wire_bytes(); let overhead = 28usize; From f3fef7bc1951bd021eb7fa359abd12bcb2da5832 Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 19 Aug 2026 13:03:46 -0400 Subject: [PATCH 10/11] tree: leaf digests commit the suffix alone; collision machinery dissolves to ingestion's assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner-ruled: a leaf's path is the full-width hash of its version, so under the uniform-hash model the suffix is already a complete commitment to the version set — committing the raw version bytes bought detection only of off-model hash collisions (excluded by the model of record) and of local path-derivation bugs the differential oracles already sample deliberately at test time. The preimage returns to LEAF_TAG ‖ suffix_len ‖ suffix. With same-position leaves digest-equal by construction, the merge walk's leaf arm is unreachable (equal pairs prune above) and becomes an assertion; join loses its error channel entirely. The apply walk keeps the one live detector — both leaves in hand at an occupied path: byte-identical re-inserts stay idempotent, disagreement asserts as version reuse (a crate bug, never an input: fresh ticks strictly dominate the ceiling, party linearity keeps regions disjoint, and no wire-derived leaf passes through the walk). LeafCollision dissolves as a type; act, react, Tree::act, Tree::join, and the Batch and gossip commits are all infallible, restoring Tree::act's original public signature. Every digest moves, so the wire snapshots re-accept as this deliberate, owner-ruled pre-release format change (digest-value movement only: line-for-line hex structure verified unchanged). --- src/batch.rs | 5 +- src/peer/gossip.rs | 9 +- src/reconciliation.rs | 11 +- src/tests.rs | 5 +- src/tree.rs | 46 +-- src/tree/arb.rs | 58 ++- src/tree/mirror/alternating/tests.rs | 2 +- .../streaming/materialized/unknown/tests.rs | 5 +- .../streaming/remote/adapter/tests/parking.rs | 3 +- .../mirror/streaming/remote/proxy/tests.rs | 28 +- .../remote/proxy/tests/declarations.rs | 52 +-- .../streaming/remote/proxy/tests/failures.rs | 13 +- .../streaming/remote/proxy/tests/greeting.rs | 45 +-- .../streaming/remote/proxy/tests/transport.rs | 13 +- src/tree/mirror/streaming/tests/fixtures.rs | 13 +- src/tree/tests.rs | 330 +++++++----------- src/tree/traverse.rs | 2 +- src/tree/traverse/act.rs | 53 ++- src/tree/traverse/join.rs | 80 ++--- src/tree/traverse/join/tests.rs | 3 +- src/tree/traverse/unknown/tests.rs | 5 +- src/tree/typed/hash.rs | 29 +- src/tree/typed/hash/tests.rs | 18 +- src/tree/typed/untyped.rs | 2 +- src/tree/typed/untyped/tests.rs | 23 +- ...ootstrap_snapshot__populated_provider.snap | 26 +- .../bootstrap_snapshot__string_payload.snap | 18 +- ...strap_snapshot__v1_populated_provider.snap | 22 +- ...etric_message_targets_unbatch_the_run.snap | 10 +- .../gossip_snapshot__batched_supply_run.snap | 10 +- ...napshot__both_redact_the_same_message.snap | 20 +- ...bulk_initiator_ships_opening_supplies.snap | 36 +- ...gossip_snapshot__converged_forks_noop.snap | 36 +- ...gossip_snapshot__deep_trie_divergence.snap | 260 +++++++------- ...shot__early_supplies_honor_redactions.snap | 36 +- .../gossip_snapshot__fork_insert_redact.snap | 36 +- .../gossip_snapshot__one_sided_transfer.snap | 18 +- .../gossip_snapshot__redaction_only.snap | 28 +- ..._same_live_content_divergent_versions.snap | 20 +- .../gossip_snapshot__string_payload.snap | 20 +- ...ossip_snapshot__v1_one_sided_transfer.snap | 16 +- .../retire_snapshot__divergent_retire.snap | 20 +- ...re_snapshot__retire_into_bootstrapper.snap | 18 +- .../retire_snapshot__v1_divergent_retire.snap | 20 +- 44 files changed, 653 insertions(+), 870 deletions(-) diff --git a/src/batch.rs b/src/batch.rs index 1b6c52aac..d56f0b7dc 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -106,10 +106,7 @@ impl Drop for Batch<'_, T> { // Notify observers iff the batch changed the tree, straight from // `act`'s changed flag: no root hash is read inside this critical // section (`Tree::act` states the flag's contract). - inner.tree.act(party, actions).expect( - "a fresh tick strictly dominates the ceiling, which bounds \ - every live leaf, so a local insert cannot collide", - ) + inner.tree.act(party, actions) }); } } diff --git a/src/peer/gossip.rs b/src/peer/gossip.rs index b1331bc45..6ec7c3c98 100644 --- a/src/peer/gossip.rs +++ b/src/peer/gossip.rs @@ -912,14 +912,7 @@ impl Peer { merged.latest().partial_cmp(inner.tree.latest()), None | Some(std::cmp::Ordering::Greater) ); - // A leaf collision in the merge is unreachable from any input: - // both trees derive paths from versions locally, so it would - // take a full-width hash collision between distinct versions - // (off-model) or a broken tree invariant in this crate. - let tree_changed = inner - .tree - .join(merged) - .expect("reconciled leaves cannot collide: paths are version-derived"); + let tree_changed = inner.tree.join(merged); peer_retiring || tree_changed || ceiling_advancing }); if party_overlap { diff --git a/src/reconciliation.rs b/src/reconciliation.rs index d10ff905a..c9d6040e7 100644 --- a/src/reconciliation.rs +++ b/src/reconciliation.rs @@ -28,11 +28,12 @@ //! 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 — is -//! detected the moment two claimants meet at one replica, and the -//! detecting operation halts: producing such a pair at all requires -//! violating the linearity invariant the crate docs' safety rules state, -//! a regime that is already fatal to causal gossip. +//! 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, diff --git a/src/tests.rs b/src/tests.rs index 1a2fe3f84..1ba9f5ac2 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -564,10 +564,7 @@ fn uncontained_supply_fails_gossip_and_poisons_the_link() { let (escaped_root, _, escaped) = crate::tree::arb::poisoned_root(&party_of(&poisoned), &base, Message::new(0u64)); poisoned.inner.send_modify(|inner| { - inner - .tree - .join(Tree { root: escaped_root }) - .expect("collision-free by construction"); + inner.tree.join(Tree { root: escaped_root }); }); assert!( !crate::tree::mirror::contained(&escaped, poisoned.inner.borrow().tree.latest()), diff --git a/src/tree.rs b/src/tree.rs index 275241686..4dacb3349 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -404,19 +404,7 @@ impl Tree { /// produce `true` without a hash change, and then the cost is one /// spurious watch wakeup, never a missed one. /// - /// # Errors - /// - /// [`traverse::LeafCollision`] if an insert lands on an occupied path - /// disagreeing on version or payload; the tree is untouched. Paths are - /// version-derived and each insert's fresh tick strictly dominates the - /// ceiling bounding every live leaf, so this is unreachable outside a - /// crate bug or an off-model hash collision — callers `expect` it, and - /// it is never user-visible ([`traverse::LeafCollision`]). - pub fn act( - &mut self, - party: &before::Party, - actions: I, - ) -> Result + pub fn act(&mut self, party: &before::Party, actions: I) -> bool where T: Send + Sync, I: IntoIterator>, @@ -472,9 +460,10 @@ 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. Errors exactly as - /// [`act`](Self::act) does, with the tree untouched on `Err`. - fn react(&mut self, reactions: I) -> Result + /// 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>>, @@ -526,11 +515,9 @@ impl Tree { let new_root = traverse::act(self.root.root.clone(), actions, |v: &Version| { new_ceiling |= v; changed = true; - })?; + }); - // The commit point: the walk returned without unwinding or erroring - // (a leaf-collision error above returns before anything of `self` - // mutates, the same atomicity as an unwind). Both fields + // The commit point: the walk returned without unwinding. Both fields // are assigned before the pre-image drops, because that drop runs // user code — everything the batch displaced becomes uniquely held // here, so its cascading `T` destructors run now, and a panicking @@ -539,7 +526,7 @@ impl Tree { let pre_image = std::mem::replace(&mut self.root.root, new_root); self.root.ceiling = new_ceiling; drop(pre_image); - Ok(changed) + changed } /// Merges `other` into `self` by a single simultaneous recursion over @@ -566,14 +553,7 @@ impl Tree { /// answers for what observers of the *set* can see, and a ceiling-only /// join leaves the set untouched. /// - /// # Errors - /// - /// [`traverse::LeafCollision`] if the two trees hold leaves at one path - /// that disagree on version or payload; this tree is untouched (hash, - /// ceiling, and content all unchanged). Unreachable outside a crate bug - /// or an off-model hash collision — callers `expect` it, and it is - /// never user-visible ([`traverse::LeafCollision`]). - pub fn join(&mut self, other: Tree) -> Result + pub fn join(&mut self, other: Tree) -> bool where T: Send + Sync, { @@ -605,13 +585,11 @@ impl Tree { &self.root.ceiling, &their_version, &mut changed, - )?; + ); let new_ceiling = &self.root.ceiling | their_version; // The commit point: the walk and the ceiling fold both completed - // without unwinding or erroring (a leaf-collision error above - // returns before anything of `self` mutates, the same atomicity as - // an unwind). Both fields are assigned before the pre-image drops, + // 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 @@ -620,7 +598,7 @@ impl Tree { let pre_image = std::mem::replace(&mut self.root.root, merged); self.root.ceiling = new_ceiling; drop(pre_image); - Ok(changed) + changed } } diff --git a/src/tree/arb.rs b/src/tree/arb.rs index 501ed15c3..0d15b0d19 100644 --- a/src/tree/arb.rs +++ b/src/tree/arb.rs @@ -79,7 +79,7 @@ pub fn arb_root_node( (path, version.clone(), Action::Insert(message)) }) .collect(); - act(None, actions, |_| ()).expect("collision-free by construction") + act(None, actions, |_| ()) }) .boxed() } @@ -151,21 +151,18 @@ pub fn arb_divergent_pair() -> BoxedStrategy<(crate::tree::Root<()>, crate::tree base.act( &p_s, (0..n_shared).map(|_| Action::Insert(Message::new(()))), - ) - .expect("collision-free by construction"); + ); 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(); - t.act(party, (0..n).map(|_| Action::Insert(Message::new(())))) - .expect("collision-free by construction"); + t.act(party, (0..n).map(|_| Action::Insert(Message::new(())))); let forgets: Vec<_> = shared_keys .iter() .zip(redact) .filter_map(|(k, &r)| r.then_some(Action::Forget(*k))) .collect(); - t.act(party, forgets) - .expect("collision-free by construction"); + t.act(party, forgets); t.root }; @@ -212,21 +209,18 @@ pub fn arb_wide_divergent_pair() -> BoxedStrategy<(crate::tree::Root<()>, crate: base.act( &p_s, (0..n_shared).map(|_| Action::Insert(Message::new(()))), - ) - .expect("collision-free by construction"); + ); 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(); - t.act(party, (0..n).map(|_| Action::Insert(Message::new(())))) - .expect("collision-free by construction"); + t.act(party, (0..n).map(|_| Action::Insert(Message::new(())))); let forgets: Vec<_> = shared_keys .iter() .zip(redact) .filter_map(|(k, &r)| r.then_some(Action::Forget(*k))) .collect(); - t.act(party, forgets) - .expect("collision-free by construction"); + t.act(party, forgets); t.root }; @@ -266,8 +260,7 @@ pub fn arb_deep_divergent_pair() -> BoxedStrategy<(crate::tree::Root<()>, crate: Action::Insert(Message::new(())), )], |_| (), - ) - .expect("collision-free by construction"); + ); // One side: `width` sibling leaves diverging at `depth`, all on // the side's own party. The branch ranges are disjoint across @@ -287,7 +280,7 @@ pub fn arb_deep_divergent_pair() -> BoxedStrategy<(crate::tree::Root<()>, crate: let node = if leaves.is_empty() { base.clone() } else { - act(base.clone(), leaves, |_| ()).expect("collision-free by construction") + act(base.clone(), leaves, |_| ()) }; root_with_ceiling(node, shared_version.clone() | version) }; @@ -394,8 +387,7 @@ pub fn early_first_child_dispute_pair() -> (crate::tree::Root<()>, crate::tree:: let build = |party: &Party, base: Version, live: usize| { let mut tree = Tree::new(); tree.root.ceiling = base; - tree.act(party, (0..live).map(|_| Action::Insert(Message::new(())))) - .expect("collision-free by construction"); + tree.act(party, (0..live).map(|_| Action::Insert(Message::new(())))); tree }; let left = build(&p_a, burnt(&p_a, at), LEFT_LEAVES); @@ -479,8 +471,7 @@ pub fn uncontained_supply_pair() -> (crate::tree::Root<()>, crate::tree::Root<() Action::Insert(receiver_message), )], |_| (), - ) - .expect("collision-free by construction"), + ), receiver_version.clone(), ); @@ -503,8 +494,7 @@ pub fn uncontained_supply_pair() -> (crate::tree::Root<()>, crate::tree::Root<() None, vec![(path, escaped.clone(), Action::Insert(message))], |_| (), - ) - .expect("collision-free by construction"), + ), declared, ); (receiver, poisoned, path, escaped) @@ -560,8 +550,7 @@ pub fn poisoned_root( None, vec![(path, escaped.clone(), Action::Insert(message))], |_| (), - ) - .expect("collision-free by construction"), + ), Version::new(), ); (root, path, escaped) @@ -592,8 +581,7 @@ pub fn leaf_parent_dispute_pair() -> ( Action::Insert(Message::new(())), )], |_| (), - ) - .expect("collision-free by construction"); + ); // Each side's extra rides its own disjoint party, so both extras are // causally concurrent with everything else and survive deletion-pruning. @@ -607,8 +595,7 @@ pub fn leaf_parent_dispute_pair() -> ( Action::Insert(Message::new(())), )], |_| (), - ) - .expect("collision-free by construction"); + ); let mut b_version = Version::new(); b_version.tick(&nth_party(2)); @@ -617,9 +604,9 @@ pub fn leaf_parent_dispute_pair() -> ( b_version.clone(), Action::Insert(Message::new(())), ); - let b_node = act(base, vec![b_extra.clone()], |_| ()).expect("collision-free by construction"); + let b_node = act(base, vec![b_extra.clone()], |_| ()); - let union = act(a_node.clone(), vec![b_extra], |_| ()).expect("collision-free by construction"); + let union = act(a_node.clone(), vec![b_extra], |_| ()); let a_ceiling = shared_version.clone() | a_version; let b_ceiling = shared_version | b_version; @@ -655,8 +642,7 @@ pub fn leaf_parent_redaction_pair() -> ( Action::Insert(Message::new(())), )], |_| (), - ) - .expect("collision-free by construction"); + ); // b: built on a's history, inserts a concurrent sibling, then forgets // a's leaf. The forget leaves no tombstone; b remembers only through its @@ -671,18 +657,16 @@ pub fn leaf_parent_redaction_pair() -> ( let mut forget_version = b_version.clone(); forget_version.tick(&nth_party(1)); let b_node = act( - act(a_node.clone(), vec![b_insert.clone()], |_| ()) - .expect("collision-free by construction"), + act(a_node.clone(), vec![b_insert.clone()], |_| ()), vec![( leaf_sibling_path(0x00), forget_version.clone(), Action::Forget, )], |_| (), - ) - .expect("collision-free by construction"); + ); - let survivor = act(None, vec![b_insert], |_| ()).expect("collision-free by construction"); + let survivor = act(None, vec![b_insert], |_| ()); let b_ceiling = a_version.clone() | forget_version; let expected = root_with_ceiling(survivor, a_version.clone() | b_ceiling.clone()); diff --git a/src/tree/mirror/alternating/tests.rs b/src/tree/mirror/alternating/tests.rs index 6441ac34d..bc4fe2cd9 100644 --- a/src/tree/mirror/alternating/tests.rs +++ b/src/tree/mirror/alternating/tests.rs @@ -261,7 +261,7 @@ proptest! { ceiling: actions .iter() .fold(Version::default(), |acc, (_, v, _)| acc | v.clone()), - root: act(None, actions.to_vec(), |_| ()).expect("collision-free by construction"), + root: act(None, actions.to_vec(), |_| ()), }; let tree_a = wrap(&actions_a); diff --git a/src/tree/mirror/streaming/materialized/unknown/tests.rs b/src/tree/mirror/streaming/materialized/unknown/tests.rs index 8756e2120..f60e0ac1a 100644 --- a/src/tree/mirror/streaming/materialized/unknown/tests.rs +++ b/src/tree/mirror/streaming/materialized/unknown/tests.rs @@ -44,10 +44,7 @@ fn tree_and_known(flags_a: &[bool], flags_b: &[bool]) -> (Option) -> [u8; MERKLE_HASH_LEN] { /// The expected reconciled union, computed by the in-memory join oracle. fn union_hash(a: &crate::tree::Root<()>, b: &crate::tree::Root<()>) -> [u8; MERKLE_HASH_LEN] { let mut union = Tree { root: a.clone() }; - union - .join(Tree { root: b.clone() }) - .expect("collision-free by construction"); + union.join(Tree { root: b.clone() }); union.hash() } @@ -49,16 +47,12 @@ fn union_hash(a: &crate::tree::Root<()>, b: &crate::tree::Root<()>) -> [u8; MERK /// initiator election under honest declarations. fn uneven_pair() -> (crate::tree::Root<()>, crate::tree::Root<()>) { let mut small = Tree::new(); - small - .act(&nth_party(1), [Action::Insert(Message::new(()))]) - .expect("collision-free by construction"); + small.act(&nth_party(1), [Action::Insert(Message::new(()))]); let mut large = Tree::new(); - large - .act( - &nth_party(0), - (0..4).map(|_| Action::Insert(Message::new(()))), - ) - .expect("collision-free by construction"); + large.act( + &nth_party(0), + (0..4).map(|_| Action::Insert(Message::new(()))), + ); (small.root, large.root) } @@ -76,16 +70,12 @@ const BULK_MESSAGES: usize = FAN + 1; /// side includes a genuinely batched multi-record run. fn batched_uneven_pair() -> (crate::tree::Root<()>, crate::tree::Root<()>) { let mut small = Tree::new(); - small - .act(&nth_party(1), [Action::Insert(Message::new(()))]) - .expect("collision-free by construction"); + small.act(&nth_party(1), [Action::Insert(Message::new(()))]); let mut large = Tree::new(); - large - .act( - &nth_party(0), - (0..BULK_MESSAGES).map(|_| Action::Insert(Message::new(()))), - ) - .expect("collision-free by construction"); + large.act( + &nth_party(0), + (0..BULK_MESSAGES).map(|_| Action::Insert(Message::new(()))), + ); (small.root, large.root) } @@ -284,19 +274,15 @@ fn understated_set_len_fails_the_session() { /// exclusive content rides the opening-supply stream as one reply. fn opening_bulk_pair() -> (crate::tree::Root<()>, crate::tree::Root<()>) { let mut small = Tree::new(); - small - .act( - &nth_party(1), - (0..4).map(|_| Action::Insert(Message::new(()))), - ) - .expect("collision-free by construction"); + small.act( + &nth_party(1), + (0..4).map(|_| Action::Insert(Message::new(()))), + ); let mut large = Tree::new(); - large - .act( - &nth_party(0), - (0..8).map(|_| Action::Insert(Message::new(()))), - ) - .expect("collision-free by construction"); + large.act( + &nth_party(0), + (0..8).map(|_| Action::Insert(Message::new(()))), + ); (small.root, large.root) } diff --git a/src/tree/mirror/streaming/remote/proxy/tests/failures.rs b/src/tree/mirror/streaming/remote/proxy/tests/failures.rs index 48670be90..8ee6a36e1 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests/failures.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests/failures.rs @@ -58,15 +58,12 @@ fn stacked_pair() -> (crate::tree::Root<()>, crate::tree::Root<()>) { left.act( &nth_party(0), (0..8).map(|_| Action::Insert(Message::new(()))), - ) - .expect("collision-free by construction"); + ); let mut right = Tree::new(); - right - .act( - &nth_party(1), - (0..8).map(|_| Action::Insert(Message::new(()))), - ) - .expect("collision-free by construction"); + right.act( + &nth_party(1), + (0..8).map(|_| Action::Insert(Message::new(()))), + ); (left.root, right.root) } diff --git a/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs b/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs index 7ce6f0484..62bca8ac9 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests/greeting.rs @@ -46,9 +46,7 @@ fn wire_reconcile( /// oracle. fn union_hash(a: &crate::tree::Root<()>, b: &crate::tree::Root<()>) -> [u8; MERKLE_HASH_LEN] { let mut union = Tree { root: a.clone() }; - union - .join(Tree { root: b.clone() }) - .expect("collision-free by construction"); + union.join(Tree { root: b.clone() }); union.hash() } @@ -117,30 +115,22 @@ fn order_by_election( fn empty_carried_listing_asks_for_everything() { // The populated responder: one message on party 0. let mut populated = Tree::new(); - populated - .act(&nth_party(0), [Action::Insert(Message::new(()))]) - .expect("collision-free by construction"); + populated.act(&nth_party(0), [Action::Insert(Message::new(()))]); // The emptied initiator: insert-then-forget on party 1 ticks its version // 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(()))]) - .expect("collision-free by construction"); + emptied.act(&nth_party(1), [Action::Insert(Message::new(()))]); 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)) - .expect("collision-free by construction"); + emptied.act(&nth_party(1), paths.into_iter().map(Action::Forget)); assert!(emptied.is_empty(), "the initiator's tree must be empty"); let expected = { let mut union = populated.clone(); - union - .join(emptied.clone()) - .expect("collision-free by construction"); + union.join(emptied.clone()); union }; let (left, right) = wire_reconcile(emptied.root, populated.root.clone()); @@ -161,8 +151,7 @@ fn empty_carried_listing_asks_for_everything() { fn converged_session_carries_listings_unused() { let build = || { let mut tree = Tree::new(); - tree.act(&nth_party(0), [Action::Insert(Message::new(()))]) - .expect("collision-free by construction"); + tree.act(&nth_party(0), [Action::Insert(Message::new(()))]); tree }; let (a, b) = (build(), build()); @@ -199,12 +188,10 @@ fn converged_session_carries_listings_unused() { fn mixed_empty_and_populated_converges() { let empty = Tree::<()>::new(); let mut populated = Tree::new(); - populated - .act( - &nth_party(0), - (0..4).map(|_| Action::Insert(Message::new(()))), - ) - .expect("collision-free by construction"); + populated.act( + &nth_party(0), + (0..4).map(|_| Action::Insert(Message::new(()))), + ); assert_ne!( populated.latest().as_bytes(), empty.latest().as_bytes(), @@ -252,8 +239,7 @@ fn overlapping_sessions_lose_innocent_leaf_after_honored_redaction() { // puts shared runs on both sides of every divergence point. let p = nth_party(0); let mut t0 = Tree::new(); - t0.act(&p, (0..25).map(|_| Action::Insert(Message::new(())))) - .expect("collision-free by construction"); + t0.act(&p, (0..25).map(|_| Action::Insert(Message::new(())))); let leaves: Vec<_> = t0 .iter() .map(|(v, _)| (crate::tree::typed::Path::for_leaf(v), v.clone())) @@ -271,8 +257,7 @@ fn overlapping_sessions_lose_innocent_leaf_after_honored_redaction() { let mut twin = Tree { root: t0.root.clone(), }; - twin.act(&nth_party(1), [Action::Forget(*k)]) - .expect("collision-free by construction"); + twin.act(&nth_party(1), [Action::Forget(*k)]); // S1's session and install: reconcile T0 against the redacting // twin over the wire, then join the result into the live tree. @@ -284,16 +269,14 @@ fn overlapping_sessions_lose_innocent_leaf_after_honored_redaction() { }; live.join(Tree { root: s1_reconciled, - }) - .expect("collision-free by construction"); + }); let expected = live.hash(); // S2's install, after S1's: joining our own causal past must be an // identity on the tree. live.join(Tree { root: s2_reconciled.clone(), - }) - .expect("collision-free by construction"); + }); if live.hash() != expected { let missing: Vec<_> = leaves diff --git a/src/tree/mirror/streaming/remote/proxy/tests/transport.rs b/src/tree/mirror/streaming/remote/proxy/tests/transport.rs index b0d3a7b17..575b8b350 100644 --- a/src/tree/mirror/streaming/remote/proxy/tests/transport.rs +++ b/src/tree/mirror/streaming/remote/proxy/tests/transport.rs @@ -28,15 +28,12 @@ fn flush_only_one_byte_transport_reconciles() { left.act( &nth_party(0), (0..8).map(|_| Action::Insert(Message::new(()))), - ) - .expect("collision-free by construction"); + ); let mut right = Tree::new(); - right - .act( - &nth_party(1), - (0..8).map(|_| Action::Insert(Message::new(()))), - ) - .expect("collision-free by construction"); + right.act( + &nth_party(1), + (0..8).map(|_| Action::Insert(Message::new(()))), + ); let expected = run_to_quiescence(reconcile_locally(left.root.clone(), right.root.clone())) .expect("materialized oracle should remain live"); let flush_only = plan(1, 1, vec![1; 512], true); diff --git a/src/tree/mirror/streaming/tests/fixtures.rs b/src/tree/mirror/streaming/tests/fixtures.rs index 26bc0cb31..d81c6ef31 100644 --- a/src/tree/mirror/streaming/tests/fixtures.rs +++ b/src/tree/mirror/streaming/tests/fixtures.rs @@ -55,7 +55,7 @@ where Action::Insert(Message::new(value.clone())), )); } - act(node, actions, |_| ()).expect("collision-free by construction") + act(node, actions, |_| ()) } /// Wrap a node as a [`Root`] whose ceiling is the node's own. @@ -349,7 +349,7 @@ pub(super) fn one_sided_pair(spec: &[(u8, u8, u8)]) -> (Root<()>, Root<()>) { )); } } - let a_node = act(None, shared, |_| ()).expect("collision-free by construction"); + let a_node = act(None, shared, |_| ()); // b's extras: a separate chain on a disjoint party, so they are causally // concurrent with a's version and survive deletion-pruning when provided. @@ -367,7 +367,7 @@ pub(super) fn one_sided_pair(spec: &[(u8, u8, u8)]) -> (Root<()>, Root<()>) { )); } } - let b_node = act(a_node.clone(), extras, |_| ()).expect("collision-free by construction"); + let b_node = act(a_node.clone(), extras, |_| ()); let root = |node: Option>| Root { ceiling: node @@ -438,7 +438,7 @@ pub(super) fn divergent_cells_pair( )); } } - let base_node = act(None, base, |_| ()).expect("collision-free by construction"); + let base_node = act(None, base, |_| ()); // Each side's extras ride their own party's chain, concurrent with the // shared chain and with each other, so both survive deletion-pruning @@ -457,9 +457,8 @@ pub(super) fn divergent_cells_pair( } actions }; - let a_node = - act(base_node.clone(), extras(2, a_slot), |_| ()).expect("collision-free by construction"); - let b_node = act(base_node, extras(1, b_slot), |_| ()).expect("collision-free by construction"); + let a_node = act(base_node.clone(), extras(2, a_slot), |_| ()); + let b_node = act(base_node, extras(1, b_slot), |_| ()); let root = |node: Option>| Root { ceiling: node diff --git a/src/tree/tests.rs b/src/tree/tests.rs index 80e2bc71b..60b7491ac 100644 --- a/src/tree/tests.rs +++ b/src/tree/tests.rs @@ -114,7 +114,7 @@ fn insert_at( /// 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 ‖ version` and +/// 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 @@ -125,10 +125,9 @@ fn reference_hash(values: &[(Version, Bytes)]) -> Hash { const BRANCH_TAG: u8 = 1; fn hash_at(depth: usize, leaves: &[([u8; 32], &Version)]) -> Hash { - if let [(path, version)] = leaves { + if let [(path, _version)] = leaves { let mut preimage = vec![LEAF_TAG, (32 - depth) as u8]; preimage.extend_from_slice(&path[depth..]); - preimage.extend_from_slice(version.as_bytes()); return Hash::of(&preimage); } @@ -198,8 +197,7 @@ fn empty_tree_hash_matches_reference() { fn single_value_hash_matches_reference() { let value = Bytes::from(&b"hello"[..]); let mut tree: Tree = Tree::new(); - tree.act(&party_of("P"), [insert_action(value.clone())]) - .expect("collision-free by construction"); + tree.act(&party_of("P"), [insert_action(value.clone())]); let tree_hash = tree.hash(); let reference = reference_hash(&[(version_for("P", 1), value)]); assert_eq!(&tree_hash, reference.as_bytes()); @@ -221,7 +219,7 @@ proptest! { .prop_map(|v| v.into_iter().map(Bytes::from).collect::>()), ) { let mut tree = Tree::new(); - tree.act(&party_of("P"), values.iter().cloned().map(insert_action)).expect("collision-free by construction"); + tree.act(&party_of("P"), values.iter().cloned().map(insert_action)); let reference_input: Vec<_> = values .into_iter() .enumerate() @@ -274,7 +272,7 @@ proptest! { // Route A: one react batch, base order. let mut direct = Tree::new(); - direct.react(kept.iter().map(versioned)).expect("collision-free by construction"); + direct.react(kept.iter().map(versioned)); // Route B: shuffled order, split into two batches, with the extra // leaves inserted in between and redacted again afterwards. @@ -284,20 +282,20 @@ proptest! { .map(|(i, b)| event(kept.len() + i, b)) .collect(); let mut detoured = Tree::new(); - detoured.react(shuffled[..cut].iter().map(versioned)).expect("collision-free by construction"); - detoured.react(extra_events.iter().map(|(k, v, m)| (*k, v.clone(), m.clone()))).expect("collision-free by construction"); - detoured.react(shuffled[cut..].iter().map(versioned)).expect("collision-free by construction"); + detoured.react(shuffled[..cut].iter().map(versioned)); + detoured.react(extra_events.iter().map(|(k, v, m)| (*k, v.clone(), m.clone()))); + detoured.react(shuffled[cut..].iter().map(versioned)); detoured.act( &party_of("P"), extra_events.iter().rev().map(|(k, _, _)| Action::Forget(*k)), - ).expect("collision-free by construction"); + ); // Route C: two disjoint halves, merged in memory. let mut joined = Tree::new(); - joined.react(kept[..cut].iter().map(versioned)).expect("collision-free by construction"); + joined.react(kept[..cut].iter().map(versioned)); let mut right = Tree::new(); - right.react(kept[cut..].iter().map(versioned)).expect("collision-free by construction"); - joined.join(right).expect("collision-free by construction"); + right.react(kept[cut..].iter().map(versioned)); + joined.join(right); let serialize = |tree: &Tree| -> Option> { tree.root @@ -360,8 +358,7 @@ proptest! { let mut all_in_one = Tree::new(); all_in_one - .react(bytes.iter().cloned().enumerate().map(event)) - .expect("collision-free by construction"); + .react(bytes.iter().cloned().enumerate().map(event)); let mut partitioned = Tree::new(); let mut chunk: Vec<(usize, Bytes)> = Vec::new(); @@ -374,7 +371,7 @@ proptest! { .into_iter() .map(event) .collect(); - partitioned.react(batch).expect("collision-free by construction"); + partitioned.react(batch); } } @@ -394,7 +391,7 @@ proptest! { ) { let mut t_act = Tree::new(); for b in &bytes { - t_act.act(&party_of("P"), [insert_action(b.clone())]).expect("collision-free by construction"); + t_act.act(&party_of("P"), [insert_action(b.clone())]); } let party = "P".to_string(); @@ -408,7 +405,7 @@ proptest! { .into_iter() .zip(bytes.iter().cloned()) .enumerate() - .map(|(i, (v, b))| insert_at(v, &party, (i + 1) as u64, b))).expect("collision-free by construction"); + .map(|(i, (v, b))| insert_at(v, &party, (i + 1) as u64, b))); prop_assert_eq!(t_act.hash(), t_react.hash()); prop_assert_eq!(t_act.latest(), t_react.latest()); @@ -429,7 +426,7 @@ proptest! { if !bytes.is_empty() { tree.act( &party_of("P"), - bytes.iter().cloned().map(insert_action)).expect("collision-free by construction"); + bytes.iter().cloned().map(insert_action)); } let n = bytes.len(); @@ -474,7 +471,7 @@ proptest! { if !bytes.is_empty() { tree.act( &party_of("P"), - bytes.iter().cloned().map(insert_action)).expect("collision-free by construction"); + bytes.iter().cloned().map(insert_action)); } // Forward order is strictly ascending by path. @@ -518,8 +515,8 @@ proptest! { let path = leaf_path(&party, 1); let mut tree = Tree::new(); - tree.act(&party_of("P"), [insert_action(value)]).expect("collision-free by construction"); - tree.act(&party_of("P"), [Action::Forget(path)]).expect("collision-free by construction"); + tree.act(&party_of("P"), [insert_action(value)]); + tree.act(&party_of("P"), [Action::Forget(path)]); prop_assert_eq!(tree.hash(), *reference_hash(&[]).as_bytes()); prop_assert_eq!(tree.latest(), version_for(&party, 2)); @@ -536,7 +533,7 @@ proptest! { let path = leaf_path(&party, 1); let mut tree = Tree::new(); - tree.act(&party_of("P"), [insert_action(value), Action::Forget(path)]).expect("collision-free by construction"); + tree.act(&party_of("P"), [insert_action(value), Action::Forget(path)]); prop_assert_eq!(tree.hash(), *reference_hash(&[]).as_bytes()); prop_assert_eq!(tree.latest(), Version::new()); @@ -557,9 +554,9 @@ proptest! { prop_assume!(!present.contains(&nuke)); let mut t_before = Tree::new(); - t_before.act(&party_of("P"), bytes.into_iter().map(insert_action)).expect("collision-free by construction"); + t_before.act(&party_of("P"), bytes.into_iter().map(insert_action)); let mut t_after = t_before.clone(); - t_after.act(&party_of("P"), [Action::Forget(nuke)]).expect("collision-free by construction"); + t_after.act(&party_of("P"), [Action::Forget(nuke)]); prop_assert_eq!(t_before.hash(), t_after.hash()); prop_assert_eq!(t_before.latest(), t_after.latest()); @@ -584,7 +581,7 @@ proptest! { for i in 0..prior_inserts { tree.act(&party_of(&party), [insert_action(Bytes::from( format!("prior-{i}").into_bytes(), - ))]).expect("collision-free by construction"); + ))]); } let actions: Vec> = (0..batch_size) @@ -592,7 +589,7 @@ proptest! { insert_action(Bytes::from(format!("batch-{i}").into_bytes())) }) .collect(); - tree.act(&party_of(&party), actions).expect("collision-free by construction"); + tree.act(&party_of(&party), actions); // Each prior insert and each batch insert ticks the party once, so the // tree's version is exactly that many ticks of the owning party. @@ -609,10 +606,10 @@ proptest! { for i in 0..prior_batches { tree.act(&party_of("P"), [insert_action(Bytes::from( format!("prior-{i}").into_bytes(), - ))]).expect("collision-free by construction"); + ))]); } let before = tree.latest().clone(); - tree.act(&party_of("P"), std::iter::empty::>()).expect("collision-free by construction"); + tree.act(&party_of("P"), std::iter::empty::>()); prop_assert_eq!(tree.latest(), before); } @@ -648,12 +645,12 @@ proptest! { .collect(); let mut t_ab = Tree::new(); - t_ab.react(batch_a.clone()).expect("collision-free by construction"); - t_ab.react(batch_b.clone()).expect("collision-free by construction"); + t_ab.react(batch_a.clone()); + t_ab.react(batch_b.clone()); let mut t_ba = Tree::new(); - t_ba.react(batch_b).expect("collision-free by construction"); - t_ba.react(batch_a).expect("collision-free by construction"); + t_ba.react(batch_b); + t_ba.react(batch_a); prop_assert_eq!(t_ab, t_ba); } @@ -679,11 +676,11 @@ proptest! { .collect(); let mut t_once = Tree::new(); - t_once.react(batch.clone()).expect("collision-free by construction"); + t_once.react(batch.clone()); let mut t_twice = Tree::new(); - t_twice.react(batch.clone()).expect("collision-free by construction"); - t_twice.react(batch).expect("collision-free by construction"); + t_twice.react(batch.clone()); + t_twice.react(batch); prop_assert_eq!(t_once, t_twice); } @@ -716,13 +713,13 @@ proptest! { t_base.react(base.iter().cloned().map(|b| { let (v, scalar) = meta_by_value.get(&b).unwrap(); insert_at(v.clone(), &party, *scalar, b) - })).expect("collision-free by construction"); + })); let mut t_shuf = Tree::new(); t_shuf.react(shuffled.iter().cloned().map(|b| { let (v, scalar) = meta_by_value.get(&b).unwrap(); insert_at(v.clone(), &party, *scalar, b) - })).expect("collision-free by construction"); + })); prop_assert_eq!(t_base, t_shuf); } @@ -750,7 +747,7 @@ proptest! { let scalar = (i + 1) as u64; let mut recorded = tree_a.latest().clone(); recorded.tick(&party_of(&a_id)); - tree_a.act(&party_of("A"), [insert_action(value.clone())]).expect("collision-free by construction"); + tree_a.act(&party_of("A"), [insert_action(value.clone())]); a_events.push(insert_at(recorded, &a_id, scalar, value.clone())); } @@ -760,12 +757,12 @@ proptest! { let scalar = (i + 1) as u64; let mut recorded = tree_b.latest().clone(); recorded.tick(&party_of(&b_id)); - tree_b.act(&party_of("B"), [insert_action(value.clone())]).expect("collision-free by construction"); + tree_b.act(&party_of("B"), [insert_action(value.clone())]); b_events.push(insert_at(recorded, &b_id, scalar, value.clone())); } - tree_a.react(b_events.iter().map(|(k, v, m)| (*k, v.clone(), m.clone()))).expect("collision-free by construction"); - tree_b.react(a_events.iter().map(|(k, v, m)| (*k, v.clone(), m.clone()))).expect("collision-free by construction"); + tree_a.react(b_events.iter().map(|(k, v, m)| (*k, v.clone(), m.clone()))); + tree_b.react(a_events.iter().map(|(k, v, m)| (*k, v.clone(), m.clone()))); prop_assert_eq!(tree_a.latest(), tree_b.latest()); prop_assert_eq!(tree_a.hash(), tree_b.hash()); @@ -777,7 +774,7 @@ proptest! { #[test] fn clone_preserves_all_observables(acts in distinct_bytes(8)) { let mut tree = Tree::new(); - tree.act(&party_of("P"), acts.into_iter().map(insert_action)).expect("collision-free by construction"); + tree.act(&party_of("P"), acts.into_iter().map(insert_action)); let cloned = tree.clone(); prop_assert_eq!(cloned.latest(), tree.latest()); @@ -795,9 +792,9 @@ proptest! { #[test] fn eq_implies_same_hash(acts in distinct_bytes(8)) { let mut t1 = Tree::new(); - t1.act(&party_of("P"), acts.iter().cloned().map(insert_action)).expect("collision-free by construction"); + t1.act(&party_of("P"), acts.iter().cloned().map(insert_action)); let mut t2 = Tree::new(); - t2.act(&party_of("P"), acts.into_iter().map(insert_action)).expect("collision-free by construction"); + t2.act(&party_of("P"), acts.into_iter().map(insert_action)); prop_assert_eq!(&t1, &t2); prop_assert_eq!(t1.hash(), t2.hash()); @@ -815,8 +812,8 @@ proptest! { let value = Bytes::from(value); let mut t_a = Tree::new(); let mut t_b = Tree::new(); - t_a.act(&party_of("A"), [insert_action(value.clone())]).expect("collision-free by construction"); - t_b.act(&party_of("B"), [insert_action(value)]).expect("collision-free by construction"); + t_a.act(&party_of("A"), [insert_action(value.clone())]); + t_b.act(&party_of("B"), [insert_action(value)]); prop_assert_ne!(t_a.hash(), t_b.hash()); } @@ -832,8 +829,8 @@ proptest! { let party = "P".to_string(); let value = Bytes::from(value); let mut tree = Tree::new(); - tree.act(&party_of("P"), [insert_action(value.clone())]).expect("collision-free by construction"); - tree.act(&party_of("P"), [insert_action(value.clone())]).expect("collision-free by construction"); + 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); let path_v2 = leaf_path(&party, 2); @@ -853,8 +850,7 @@ proptest! { #[test] fn delete_nonexistent_key() { let mut tree: Tree<()> = Tree::new(); - tree.act(&party_of("P"), [Action::Forget(Path::from([0; 32]))]) - .expect("collision-free by construction"); + tree.act(&party_of("P"), [Action::Forget(Path::from([0; 32]))]); assert_eq!(tree, Tree::new()); } @@ -1030,7 +1026,7 @@ proptest! { for (i, value) in values.iter().enumerate() { // Rotating parties makes sibling versions concurrent, not just // points on one chain, so branch maxima genuinely compare. - tree.act(&party_of([b'a' + (i % 5) as u8]), [insert_action(value.clone())]).expect("collision-free by construction"); + tree.act(&party_of([b'a' + (i % 5) as u8]), [insert_action(value.clone())]); prop_assert_eq!(tree.max_version_bytes(), naive_max_version_bytes(&tree)); } @@ -1053,8 +1049,7 @@ proptest! { } }) .unwrap_or(version); - tree.act(&party_of("P"), [Action::Forget(Path::for_leaf(&version))]) - .expect("collision-free by construction"); + tree.act(&party_of("P"), [Action::Forget(Path::for_leaf(&version))]); prop_assert_eq!(tree.max_version_bytes(), naive_max_version_bytes(&tree)); } } @@ -1078,11 +1073,11 @@ proptest! { ) { let mut left: Tree = Tree::new(); for value in &left_values { - left.act(&party_of("A"), [insert_action(value.clone())]).expect("collision-free by construction"); + left.act(&party_of("A"), [insert_action(value.clone())]); } let mut right: Tree = Tree::new(); for value in &right_values { - right.act(&party_of("B"), [insert_action(value.clone())]).expect("collision-free by construction"); + right.act(&party_of("B"), [insert_action(value.clone())]); } // A fork of `right` that `left` first absorbs wholesale: the @@ -1091,7 +1086,7 @@ proptest! { // the deletion-honoring arm, aimed at the argmax half the time // so the resize-down direction is exercised through the merge. let absorbed = right.clone(); - left.join(absorbed).expect("collision-free by construction"); + left.join(absorbed); prop_assert_eq!(left.max_version_bytes(), naive_max_version_bytes(&left)); for forget in forgets { @@ -1108,11 +1103,10 @@ proptest! { else { break; }; - left.act(&party_of("A"), [Action::Forget(Path::for_leaf(&version))]) - .expect("collision-free by construction"); + left.act(&party_of("A"), [Action::Forget(Path::for_leaf(&version))]); } - left.join(right).expect("collision-free by construction"); + left.join(right); prop_assert_eq!(left.max_version_bytes(), naive_max_version_bytes(&left)); } } @@ -1144,7 +1138,7 @@ proptest! { tree.act( &party_of("A"), base_values.iter().cloned().map(insert_action), - ).expect("collision-free by construction"); + ); let live: Vec = tree.iter().map(|(v, _)| Path::for_leaf(v)).collect(); let mut actions: Vec> = @@ -1160,7 +1154,7 @@ proptest! { actions.extend(forget_missing.into_iter().map(Action::Forget)); let before = tree.hash(); - let changed = tree.act(&party_of("A"), actions).expect("collision-free by construction"); + let changed = tree.act(&party_of("A"), actions); prop_assert_eq!(changed, tree.hash() != before); } @@ -1182,7 +1176,7 @@ proptest! { ) { let mut tree = Tree { root: a }; let before = tree.hash(); - let changed = tree.join(Tree { root: b }).expect("collision-free by construction"); + let changed = tree.join(Tree { root: b }); prop_assert_eq!(changed, tree.hash() != before); } @@ -1200,7 +1194,7 @@ proptest! { ) { let mut tree = Tree { root: a }; let before = tree.hash(); - let changed = tree.join(Tree { root: b }).expect("collision-free by construction"); + let changed = tree.join(Tree { root: b }); prop_assert_eq!(changed, tree.hash() != before); } } @@ -1219,9 +1213,7 @@ fn deep_divergent_join_changed_flag_is_exact() { for (receiver, counter) in [(a.clone(), b.clone()), (b, a.clone())] { let mut tree = Tree { root: receiver }; let before = tree.hash(); - let changed = tree - .join(Tree { root: counter }) - .expect("collision-free by construction"); + let changed = tree.join(Tree { root: counter }); assert_eq!(changed, tree.hash() != before, "deep gain is biconditional"); assert!(changed, "a deep gain must report changed"); } @@ -1230,9 +1222,7 @@ fn deep_divergent_join_changed_flag_is_exact() { // counterparty has, so the full-depth divergent descent nets nothing. let mut tree = Tree { root: expected }; let before = tree.hash(); - let changed = tree - .join(Tree { root: a }) - .expect("collision-free by construction"); + let changed = tree.join(Tree { root: a }); assert_eq!( changed, tree.hash() != before, @@ -1244,9 +1234,7 @@ fn deep_divergent_join_changed_flag_is_exact() { let (a, b, _survivor) = crate::tree::arb::leaf_parent_redaction_pair(); let mut tree = Tree { root: a }; let before = tree.hash(); - let changed = tree - .join(Tree { root: b }) - .expect("collision-free by construction"); + let changed = tree.join(Tree { root: b }); assert_eq!( changed, tree.hash() != before, @@ -1264,24 +1252,19 @@ fn deep_divergent_join_changed_flag_is_exact() { #[test] fn ceiling_only_join_reports_unchanged() { let mut tree: Tree = Tree::new(); - tree.act(&party_of("A"), [insert_action(Bytes::from_static(b"kept"))]) - .expect("collision-free by construction"); + tree.act(&party_of("A"), [insert_action(Bytes::from_static(b"kept"))]); // The counterparty: a tree that sent one message on its own disjoint // party and then redacted it, leaving no content but an advanced // 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"))]) - .expect("collision-free by construction"); + other.act(&party_of("B"), [insert_action(Bytes::from_static(b"gone"))]); let version = other .iter() .map(|(v, _)| v.clone()) .next() .expect("one live message"); - other - .act(&party_of("B"), [Action::Forget(Path::for_leaf(&version))]) - .expect("collision-free by construction"); + other.act(&party_of("B"), [Action::Forget(Path::for_leaf(&version))]); assert!( other.is_empty(), "the counterparty redacted its only message" @@ -1289,7 +1272,7 @@ fn ceiling_only_join_reports_unchanged() { let before = tree.hash(); let ceiling_before = tree.latest().clone(); - let changed = tree.join(other).expect("collision-free by construction"); + let changed = tree.join(other); assert!( !changed, "a merge that teaches the set nothing reports unchanged", @@ -1322,15 +1305,12 @@ fn act_changed_flag_is_conservative_only_in_a_poisoned_store() { let mut tree = Tree { root: receiver }; assert!( - tree.join(Tree { root: poisoned }) - .expect("collision-free by construction"), + tree.join(Tree { root: poisoned }), "planting the escaped leaf is a real change", ); let before = tree.hash(); - let changed = tree - .act(&receiver_party, [Action::Forget(key)]) - .expect("collision-free by construction"); + let changed = tree.act(&receiver_party, [Action::Forget(key)]); assert!( changed, "the skipped forget reports changed: the conservative direction", @@ -1365,8 +1345,7 @@ fn escaped_version_defeats_redaction_in_a_poisoned_store() { // 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 }) - .expect("collision-free by construction"); + tree.join(Tree { root: poisoned }); assert!( tree.get(&escaped).is_some(), "the join plants the escaped leaf" @@ -1378,8 +1357,7 @@ fn escaped_version_defeats_redaction_in_a_poisoned_store() { // Redaction is silently skipped: the forget's version ticks from the // ceiling, which the escaped version strictly dominates. - tree.act(&receiver_party, [Action::Forget(key)]) - .expect("collision-free by construction"); + tree.act(&receiver_party, [Action::Forget(key)]); assert!( tree.get(&escaped).is_some(), "redacting the escaped leaf is silently skipped", @@ -1389,7 +1367,7 @@ fn escaped_version_defeats_redaction_in_a_poisoned_store() { // receives it on merge, because no ceiling ever classifies it as // already-seen-and-deleted. let mut fresh: Tree<()> = Tree::new(); - fresh.join(tree).expect("collision-free by construction"); + fresh.join(tree); assert!( fresh.get(&escaped).is_some(), "the escaped leaf re-plants into a fresh replica", @@ -1442,8 +1420,7 @@ mod span_door_traffic { let (equal, empty, comparable, concurrent) = cells(|| { let mut tree: Tree = Tree::new(); for round in 0..8 { - tree.act(&party_of("A"), batch("a", round, 64).map(insert_action)) - .expect("collision-free by construction"); + tree.act(&party_of("A"), batch("a", round, 64).map(insert_action)); tree.warm_caches(); } }); @@ -1475,20 +1452,17 @@ mod span_door_traffic { for label in ["A", "B", "C", "D"] { let mut tree: Tree = Tree::new(); for round in 0..4 { - tree.act(&party_of(label), batch(label, round, 32).map(insert_action)) - .expect("collision-free by construction"); + tree.act(&party_of(label), batch(label, round, 32).map(insert_action)); } tree.warm_caches(); - merged.join(tree).expect("collision-free by construction"); + merged.join(tree); } merged.warm_caches(); // Incremental rounds on the merged tree: acts invalidate // ancestor memos, so re-warming re-folds them against the // merged population. for round in 100..104 { - merged - .act(&party_of("A"), batch("a", round, 32).map(insert_action)) - .expect("collision-free by construction"); + merged.act(&party_of("A"), batch("a", round, 32).map(insert_action)); merged.warm_caches(); } }); @@ -1525,8 +1499,7 @@ fn act_unwind_leaves_tree_byte_identical() { tree.act( &party_of("P"), [insert_action(Bytes::from_static(b"survivor"))], - ) - .expect("collision-free by construction"); + ); let hash_before = tree.hash(); let ceiling_before = tree.latest().clone(); assert!(!tree.is_empty()); @@ -1540,8 +1513,7 @@ fn act_unwind_leaves_tree_byte_identical() { panic!("injected: actions iterator panics mid-drain") })); let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - tree.act(&party_of("P"), panicking_actions) - .expect("collision-free by construction"); + tree.act(&party_of("P"), panicking_actions); })); assert!(unwound.is_err(), "the injected panic must unwind out"); @@ -1579,16 +1551,13 @@ fn join_unwind_leaves_tree_byte_identical() { ours.act( &party_of("A"), [b"ours-1" as &[u8], b"ours-2", b"ours-3"].map(|b| insert_action(Bytes::from_static(b))), - ) - .expect("collision-free by construction"); + ); let mut theirs: Tree = Tree::new(); - theirs - .act( - &party_of("B"), - [b"theirs-1" as &[u8], b"theirs-2", b"theirs-3"] - .map(|b| insert_action(Bytes::from_static(b))), - ) - .expect("collision-free by construction"); + theirs.act( + &party_of("B"), + [b"theirs-1" as &[u8], b"theirs-2", b"theirs-3"] + .map(|b| insert_action(Bytes::from_static(b))), + ); let hash_before = ours.hash(); let ceiling_before = ours.latest().clone(); assert!(!ours.is_empty()); @@ -1598,7 +1567,7 @@ fn join_unwind_leaves_tree_byte_identical() { // already merged into the root frame's copied fan. let _fuse = super::panic_injection::arm(3); let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - ours.join(theirs).expect("collision-free by construction"); + ours.join(theirs); })); assert!( unwound.is_err(), @@ -1670,8 +1639,7 @@ fn act_mid_walk_unwind_leaves_tree_byte_identical() { tree.act( &party_of("A"), [b"held-1" as &[u8], b"held-2", b"held-3"].map(|b| insert_action(Bytes::from_static(b))), - ) - .expect("collision-free by construction"); + ); let hash_before = tree.hash(); let ceiling_before = tree.latest().clone(); assert!(!tree.is_empty()); @@ -1686,8 +1654,7 @@ fn act_mid_walk_unwind_leaves_tree_byte_identical() { tree.act( &party_of("A"), [b"new-1" as &[u8], b"new-2", b"new-3"].map(|b| insert_action(Bytes::from_static(b))), - ) - .expect("collision-free by construction"); + ); })); assert!( unwound.is_err(), @@ -1723,8 +1690,7 @@ fn act_destructor_unwind_leaves_tree_byte_identical() { let mut tree: Tree = Tree::new(); let existing = Message::new(DropBomb { armed: false }); let key = Path::for_leaf(&version_for("A", 2)); - tree.react([(key, version_for("A", 2), existing)]) - .expect("collision-free by construction"); + tree.react([(key, version_for("A", 2), existing)]); let hash_before = tree.hash(); let ceiling_before = tree.latest().clone(); @@ -1735,8 +1701,7 @@ fn act_destructor_unwind_leaves_tree_byte_identical() { // last handle — mid-walk. let bomb = Message::new(DropBomb { armed: true }); let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - tree.react([(key, version_for("A", 1), bomb)]) - .expect("collision-free by construction"); + tree.react([(key, version_for("A", 1), bomb)]); })); let payload = unwound.expect_err("the armed destructor must unwind out of the apply walk"); assert_eq!( @@ -1778,22 +1743,19 @@ fn join_destructor_unwind_leaves_tree_byte_identical() { ours.act( &party_of("A"), [Action::Insert(Message::new(DropBomb { armed: false }))], - ) - .expect("collision-free by construction"); + ); let bomb = Message::new(DropBomb { armed: true }); // 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 = Path::for_leaf(&version_for("A", 2)); - ours.act(&party_of("A"), [Action::Insert(bomb)]) - .expect("collision-free by construction"); + ours.act(&party_of("A"), [Action::Insert(bomb)]); // The counterparty forks while the bomb is live: the clone shares our // nodes (no `T` code runs), and after our forget below releases our // handles, the counterparty holds the bomb's only ones. let theirs = ours.clone(); - ours.act(&party_of("A"), [Action::Forget(bomb_key)]) - .expect("collision-free by construction"); + ours.act(&party_of("A"), [Action::Forget(bomb_key)]); let hash_before = ours.hash(); let ceiling_before = ours.latest().clone(); @@ -1803,7 +1765,7 @@ fn join_destructor_unwind_leaves_tree_byte_identical() { // (A at 3) and we lack its content, so deletion honoring drops the // incoming leaf mid-walk: the last handle, the armed destructor. let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - ours.join(theirs).expect("collision-free by construction"); + ours.join(theirs); })); let payload = unwound.expect_err("the armed destructor must unwind out of the merge walk"); assert_eq!( @@ -1823,70 +1785,55 @@ fn join_destructor_unwind_leaves_tree_byte_identical() { ); } -/// `Tree::join` halts with a `LeafCollision` when the two trees bind one -/// path to *different versions* — the shape only a full-width path-hash -/// collision (or a crate bug) can produce — and leaves the receiving tree -/// untouched. +/// 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 colliding pair is planted directly through `react` at a synthetic -/// shared path, which no public insert can mint; the leaf digest commits -/// the version, so the merge walk descends to the pair instead of pruning -/// it as equal. +/// 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_detects_a_version_collision_at_one_path() { +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")))]) - .expect("first insert at a fresh path cannot collide"); + 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")))]) - .expect("first insert at a fresh path cannot collide"); + theirs.react([(shared, version_for("B", 1), msg(Bytes::from_static(b"b")))]); let hash_before = ours.hash(); - let ceiling_before = ours.latest().clone(); - let collision = ours - .join(theirs) - .expect_err("distinct versions at one path must halt the merge"); - // The diagnostic names the resident leaf by its version-derived path - // (the synthetic location itself is not recoverable at the leaf level). - assert_eq!( - collision.path, - <[u8; 32]>::from(Path::for_leaf(&version_for("A", 1))) - ); - assert_eq!(ours.hash(), hash_before, "the tree is untouched on Err"); - assert_eq!( - ours.latest(), - &ceiling_before, - "the ceiling is untouched on Err" - ); + 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 the detected (version-mismatch) case stays -/// explicit. +/// 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")))]) - .expect("first insert at a fresh path cannot collide"); + 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")))]) - .expect("first insert at a fresh path cannot collide"); + theirs.react([(path, version, msg(Bytes::from_static(b"theirs")))]); let hash_before = ours.hash(); - let changed = ours - .join(theirs) - .expect("digest-equal leaves prune before the leaf arm"); + let changed = ours.join(theirs); assert!(!changed, "a digest-equal pair teaches the set nothing"); assert_eq!(ours.hash(), hash_before); let (_, message) = ours @@ -1907,49 +1854,36 @@ fn reinserting_an_identical_leaf_is_idempotent() { let message = msg(Bytes::from_static(b"same")); let mut tree: Tree = Tree::new(); - tree.react([(path, version.clone(), message.clone())]) - .expect("first insert at a fresh path cannot collide"); + tree.react([(path, version.clone(), message.clone())]); let hash_before = tree.hash(); - tree.react([(path, version, message)]) - .expect("a byte-identical re-insert is idempotent"); + 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: `react` halts with a `LeafCollision` -/// naming the path, and the tree is untouched. +/// one version is version reuse: the apply walk asserts (a crate bug, +/// never an input — every production insert carries a freshly minted +/// version). #[test] -fn react_detects_version_reuse_at_an_occupied_path() { +#[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")))]) - .expect("first insert at a fresh path cannot collide"); - let hash_before = tree.hash(); - let collision = tree - .react([(path, version, msg(Bytes::from_static(b"second")))]) - .expect_err("a second payload under one version must halt the apply"); - assert_eq!(collision.path, <[u8; 32]>::from(path)); - assert_eq!(tree.hash(), hash_before, "the tree is untouched on Err"); + 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) halts with a `LeafCollision` too: both legs of the -/// identity check are enforced, not just payload equality. +/// path collision) trips the same assertion: both legs of the identity +/// check are enforced, not just payload equality. #[test] -fn react_detects_a_path_collision_between_distinct_versions() { +#[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")))]) - .expect("first insert at a fresh path cannot collide"); - let collision = tree - .react([(shared, version_for("B", 1), msg(Bytes::from_static(b"a")))]) - .expect_err("a distinct version at an occupied path must halt the apply"); - // The diagnostic names the incoming insert by its version-derived path. - assert_eq!( - collision.path, - <[u8; 32]>::from(Path::for_leaf(&version_for("B", 1))) - ); + 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.rs b/src/tree/traverse.rs index 2815b4d9f..3cab05cfc 100644 --- a/src/tree/traverse.rs +++ b/src/tree/traverse.rs @@ -16,4 +16,4 @@ pub use act::{Action, act}; pub(crate) mod unknown; mod join; -pub use join::{LeafCollision, join}; +pub use join::join; diff --git a/src/tree/traverse/act.rs b/src/tree/traverse/act.rs index c5e6e96cd..bfa665770 100644 --- a/src/tree/traverse/act.rs +++ b/src/tree/traverse/act.rs @@ -2,7 +2,6 @@ use itertools::Itertools; use crate::{Version, message::Message}; -use super::join::LeafCollision; use super::typed::*; use height::{Height, Root, S, Z}; @@ -25,24 +24,27 @@ 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. /// -/// # Errors +/// # Panics /// -/// [`LeafCollision`] if an insert lands on a live leaf disagreeing with it -/// on version or payload (unreachable from any input; see -/// [`LeafCollision`]). On `Err` nothing has been published: the caller's -/// commit point is never reached. +/// 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, mut on_action: F, -) -> Result>, LeafCollision> +) -> Option> where T: Send + Sync, F: FnMut(&Version), 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)] @@ -63,7 +65,7 @@ pub trait Act: Height { node: Option>, actions: I, on_action: &mut F, - ) -> Result>, LeafCollision> + ) -> Option> where T: Send + Sync, F: FnMut(&Version), @@ -78,7 +80,7 @@ where node: Option>>, actions: I, on_action: &mut F, - ) -> Result>>, LeafCollision> + ) -> Option>> where T: Send + Sync, F: FnMut(&Version), @@ -135,15 +137,13 @@ where continue; } - if let Some(child) = Act::act(existing_child, actions, on_action)? { + if let Some(child) = Act::act(existing_child, actions, on_action) { updated.push((radix, child)); } } // Re-assemble: updated children + untouched existing children. - Ok(Node::branch( - updated.into_iter().chain(existing_children).collect(), - )) + Node::branch(updated.into_iter().chain(existing_children).collect()) } } @@ -152,7 +152,7 @@ impl Act for Z { mut node: Option>, actions: I, on_action: &mut F, - ) -> Result>, LeafCollision> + ) -> Option> where T: Send + Sync, F: FnMut(&Version), @@ -183,18 +183,17 @@ impl Act for Z { // 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); any mismatch is a - // `LeafCollision` — unreachable except through a crate bug or - // an off-model hash collision (see `LeafCollision`), and - // errored before anything commits. + // 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) { - if *existing.ceiling() != version - || existing.message().as_slice() != value.as_slice() - { - return Err(LeafCollision { - path: Path::for_leaf(&version).into(), - }); - } + 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; } @@ -212,6 +211,6 @@ impl Act for Z { _ => on_action(&greatest_version), } - Ok(node) + node } } diff --git a/src/tree/traverse/join.rs b/src/tree/traverse/join.rs index 5eb3b8f98..8b6fa74d5 100644 --- a/src/tree/traverse/join.rs +++ b/src/tree/traverse/join.rs @@ -19,8 +19,8 @@ //! 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 hold the same version -//! set (hashes commit shape and versions), hence the same messages; -//! keep one verbatim. +//! 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 @@ -40,26 +40,6 @@ use super::typed::*; use super::unknown::Unknown; use height::{Height, Root, S, Z}; -/// Crate-internal: two live leaves met at one tree path while disagreeing -/// on version or payload. -/// -/// Never user-visible, because no input can produce it: paths are -/// full-width hashes of versions, live leaf versions never exceed the -/// ceiling a fresh tick strictly dominates, and ingestion enforces -/// containment — so a collision requires a bug in this crate or a -/// full-width hash collision (off-model). The traversals return it as a -/// typed error so tests can construct and observe the detector directly; -/// the public seams (`Batch`'s drop commit, the gossip commit) `expect` it -/// away as the invariant breach it is. -#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] -#[error("two distinct leaves collided at one tree path: a version was reused")] -pub struct LeafCollision { - /// The 32-byte version-derived path naming one colliding leaf: the - /// resident leaf's in the merge walk, the incoming insert's in the - /// apply walk. - pub path: [u8; 32], -} - /// Merges two trees rooted at `a` and `b` into one. /// /// `a_version` / `b_version` are the two roots' version vectors, used to honor @@ -74,25 +54,19 @@ pub struct LeafCollision { /// 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. -/// -/// # Errors -/// -/// [`LeafCollision`] if two leaves meet at one path while disagreeing on -/// version or payload (unreachable from any input; see [`LeafCollision`]). -/// On `Err`, `changed` may have been set but nothing has been published: -/// the caller's commit point is never reached. pub fn join( a: Option>, b: Option>, a_version: &Version, b_version: &Version, changed: &mut bool, -) -> Result>, LeafCollision> +) -> Option> 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)] @@ -114,7 +88,7 @@ pub trait Join: Unknown { a_version: &Version, b_version: &Version, changed: &mut bool, - ) -> Result>, LeafCollision> + ) -> Option> where T: Send + Sync; } @@ -129,7 +103,7 @@ where a_version: &Version, b_version: &Version, changed: &mut bool, - ) -> Result>>, LeafCollision> + ) -> Option>> where T: Send + Sync, { @@ -139,7 +113,7 @@ where #[cfg(test)] crate::tree::panic_injection::fire_if_armed(); - Ok(match (a, b) { + match (a, b) { (None, None) => None, // Asymmetric cases: a subtree one side holds and the other lacks. // Filter it against the *other* side's version vector to honor @@ -167,7 +141,7 @@ where // hash-free), else on the Merkle hash — same version set, hence // same messages: nothing to learn on either side. if ours == theirs { - return Ok(Some(ours)); + return Some(ours); } // Differing subtrees: descend one level, merge-walking the @@ -216,7 +190,7 @@ where continue; } - match Join::join(our_child, their_child, a_version, b_version, changed)? { + match Join::join(our_child, their_child, a_version, b_version, changed) { Some(child) => { merged.insert(radix, child); } @@ -228,7 +202,7 @@ where Node::branch(merged) } - }) + } } } @@ -239,11 +213,11 @@ impl Join for Z { a_version: &Version, b_version: &Version, changed: &mut bool, - ) -> Result>, LeafCollision> + ) -> Option> where T: Send + Sync, { - Ok(match (a, b) { + match (a, b) { (None, None) => None, // The leaf-level base of the asymmetric arms' change detection: // our leaf dropped by deletion honoring is a change, and their @@ -258,26 +232,16 @@ impl Join for Z { *changed |= gained.is_some(); gained } - // Two leaves at one path are the same leaf: the path - // is the full-width hash of the version (`Path::for_leaf`), so - // one path is one version, and one version is one message. - // Verify both legs instead of assuming them; a mismatch is a - // `LeafCollision`, unreachable except through a crate bug or an - // off-model hash collision (see `LeafCollision`), and halting - // beats silently keeping a side. (A same-version pair with - // different payloads digests equal and prunes above — digests - // are content-blind by design, a modeled trade.) - (Some(ours), Some(theirs)) => { - if ours.ceiling() != theirs.ceiling() - || ours.message().as_slice() != theirs.message().as_slice() - { - return Err(LeafCollision { - path: Path::for_leaf(ours.ceiling()).into(), - }); - } - 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/join/tests.rs b/src/tree/traverse/join/tests.rs index 1f49ceac5..ec464b8b0 100644 --- a/src/tree/traverse/join/tests.rs +++ b/src/tree/traverse/join/tests.rs @@ -26,8 +26,7 @@ fn mirror_merge(a: Root<()>, b: Root<()>) -> Root<()> { /// Merges via `Tree::join`. fn join_tree(a: Root<()>, b: Root<()>) -> Root<()> { let mut a = Tree { root: a }; - a.join(Tree { root: b }) - .expect("collision-free by construction"); + a.join(Tree { root: b }); a.root } diff --git a/src/tree/traverse/unknown/tests.rs b/src/tree/traverse/unknown/tests.rs index 7c628c317..476d466bf 100644 --- a/src/tree/traverse/unknown/tests.rs +++ b/src/tree/traverse/unknown/tests.rs @@ -111,10 +111,7 @@ fn wide_divergence( } } - ( - act(None, actions, |_| ()).expect("collision-free by construction"), - known, - ) + (act(None, actions, |_| ()), known) } /// `body`'s result with its scanned-bits reading, on a fresh counter. diff --git a/src/tree/typed/hash.rs b/src/tree/typed/hash.rs index 435581d3b..3733a1187 100644 --- a/src/tree/typed/hash.rs +++ b/src/tree/typed/hash.rs @@ -84,37 +84,34 @@ impl Hash { } /// The hash of a leaf observed from the top of its compressed `suffix`: - /// `blake3(LEAF_TAG ‖ suffix_len ‖ suffix ‖ version)`. + /// `blake3(LEAF_TAG ‖ suffix_len ‖ suffix)`. /// /// `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). `version` is the leaf's canonical encoding: - /// self-delimiting, so the preimage stays injective with the suffix - /// length-tagged and the version last. + /// 32-byte path). /// - /// A leaf commits its path bytes and its version, never its message - /// bytes: every compared digest is a pure function of the version set, - /// and a content author contributes no bit to any compared quantity. - /// The path already commits the version through its hash - /// ([`Path::for_leaf`](super::Path::for_leaf)); committing the raw - /// version bytes too makes two *distinct* versions that collided into - /// one path (off-model) digest-unequal, so the merge walk surfaces - /// that impossibility as a local violation instead of keeping a side. + /// 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 /// /// Panics if `suffix` exceeds 255 bytes. Unreachable through the typed /// tree, whose height cap bounds compressed spans at the 32-byte path. - pub fn leaf(suffix: &[u8], version: &crate::Version) -> Self { - let version = version.as_bytes(); + pub fn leaf(suffix: &[u8]) -> Self { let suffix_len = u8::try_from(suffix.len()).expect("a compressed span fits in one length byte"); - let mut buf = Vec::with_capacity(2 + suffix.len() + version.len()); + let mut buf = Vec::with_capacity(2 + suffix.len()); buf.push(LEAF_TAG); buf.push(suffix_len); buf.extend_from_slice(suffix); - buf.extend_from_slice(version); Hash::of(&buf) } diff --git a/src/tree/typed/hash/tests.rs b/src/tree/typed/hash/tests.rs index beef9f733..78a104dcd 100644 --- a/src/tree/typed/hash/tests.rs +++ b/src/tree/typed/hash/tests.rs @@ -27,16 +27,17 @@ fn branch_preimage_layout() { assert_eq!(Hash::branch(&prefix, children), Hash::of(&expected)); } -/// A leaf commits to exactly `LEAF_TAG ‖ suffix_len ‖ suffix ‖ version` — -/// its compressed suffix, length-tagged, then the version's canonical -/// bytes, and never any message bytes. +/// A leaf commits to exactly `LEAF_TAG ‖ suffix_len ‖ suffix` — its +/// 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]; - let version = crate::Version::try_from(5).expect("a small scalar version is valid"); - let mut expected = vec![LEAF_TAG, 4, 0x01, 0x02, 0x03, 0x04]; - expected.extend_from_slice(version.as_bytes()); - assert_eq!(Hash::leaf(&suffix, &version), Hash::of(&expected)); + 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 — @@ -53,8 +54,7 @@ fn empty_root_is_the_empty_branch() { /// load-bearing under the single-preimage rule. #[test] fn empty_suffix_leaf_is_not_the_empty_root() { - let version = crate::Version::new(); - assert_ne!(Hash::leaf(&[], &version), Hash::empty_root()); + assert_ne!(Hash::leaf(&[]), Hash::empty_root()); } /// A prefix byte cannot masquerade as child-record bytes: two branches whose diff --git a/src/tree/typed/untyped.rs b/src/tree/typed/untyped.rs index 562692316..1d7b62ed6 100644 --- a/src/tree/typed/untyped.rs +++ b/src/tree/typed/untyped.rs @@ -470,7 +470,7 @@ impl Node { // path). let prefix: ArrayVec<[u8; 32]> = self.inner.prefix.iter().rev().copied().collect(); match &self.inner.children { - Children::Leaf { version, .. } => Hash::leaf(&prefix, version), + Children::Leaf { .. } => Hash::leaf(&prefix), Children::Branch { children, .. } => Hash::branch( &prefix, children.iter().map(|(radix, child)| (radix, child.hash())), diff --git a/src/tree/typed/untyped/tests.rs b/src/tree/typed/untyped/tests.rs index 75aec57cb..0afdc8571 100644 --- a/src/tree/typed/untyped/tests.rs +++ b/src/tree/typed/untyped/tests.rs @@ -493,7 +493,7 @@ proptest! { /// always has >= 2 children, by the path-compression invariant), so its /// index accumulates onto the reference prefix in path order until the /// underlying leaf or true branch point is reached. The preimage is then -/// assembled by hand — `LEAF_TAG ‖ len ‖ prefix ‖ version` for a leaf, +/// assembled by hand — `LEAF_TAG ‖ len ‖ prefix` for a leaf, /// `BRANCH_TAG ‖ len ‖ prefix ‖ count(u16 BE) ‖ (radix ‖ hash)*` for a /// branch — with every child hash computed by the same reference /// recursively, never by [`Node::hash`]. @@ -503,10 +503,9 @@ fn reference_hash(mut node: Node<()>) -> super::Hash { let mut prefix: Vec = Vec::new(); loop { node = match node.into_children() { - Err(leaf) => { + Err(_leaf) => { let mut buf = vec![LEAF_TAG, u8::try_from(prefix.len()).expect("short prefix")]; buf.extend_from_slice(&prefix); - buf.extend_from_slice(leaf.ceiling().as_bytes()); return super::Hash::of(&buf); } Ok(children) if children.len() == 1 => { @@ -547,9 +546,9 @@ fn full_depth_paths() -> impl Strategy> { /// The canonical tree over `paths` observed from `depth`, built from /// scratch by the maximally-compressing bulk constructor. /// -/// Leaf versions are all genesis: each leaf preimage commits the version's -/// canonical bytes as a constant tail here, so the shape properties under -/// test are isolated from version variation. +/// Leaf versions are all genesis: the hash convention never commits a +/// version, so varying them adds nothing to the hash properties checked +/// against this reference. fn canonical_at(depth: usize, paths: &[[u8; 32]]) -> Node<()> { let mut entries: Vec<([u8; 32], Option>)> = paths .iter() @@ -596,17 +595,14 @@ 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); - let mut preimage = vec![LEAF_TAG, 2, 0xBB, 0xAA]; - preimage.extend_from_slice(Version::new().as_bytes()); - assert_eq!(wrapped.hash(), super::Hash::of(&preimage)); + assert_eq!(wrapped.hash(), super::Hash::of(&[LEAF_TAG, 2, 0xBB, 0xAA])); } /// A hand-built two-leaf tree pins the preimages end to end. /// -/// Each leaf commits its length-tagged 29-byte suffix and its (genesis) -/// version's canonical bytes, and the root branch commits its 2-byte shared -/// prefix in path order, the big-endian `u16` child count, and both -/// ascending `radix ‖ hash` records. +/// Each leaf commits its length-tagged 29-byte suffix, and the root branch +/// commits its 2-byte shared prefix in path order, the big-endian `u16` +/// child count, and both ascending `radix ‖ hash` records. #[test] fn small_tree_hash_matches_byte_literal_preimage() { const LEAF_TAG: u8 = 0; @@ -621,7 +617,6 @@ fn small_tree_hash_matches_byte_literal_preimage() { let leaf_hash = |suffix: &[u8]| { let mut buf = vec![LEAF_TAG, u8::try_from(suffix.len()).expect("short suffix")]; buf.extend_from_slice(suffix); - buf.extend_from_slice(Version::new().as_bytes()); super::Hash::of(&buf) }; // Root: prefix [1, 2] (path order), two children at radixes 3 and 7, diff --git a/tests/snapshots/bootstrap_snapshot__populated_provider.snap b/tests/snapshots/bootstrap_snapshot__populated_provider.snap index c125ac8a9..1542400b4 100644 --- a/tests/snapshots/bootstrap_snapshot__populated_provider.snap +++ b/tests/snapshots/bootstrap_snapshot__populated_provider.snap @@ -16,20 +16,20 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 92 listing: 3 child(ren) - child 0x7c: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 - child 0x88: 483427dad201088bf07cb3fe2a1ff18f52ac946ab7aa993a - child 0x9a: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa + child 0x88: cf55a19b320fee28bbdd5998cabc6c0000caeb8c569ad975 + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 listing frame: 79 bytes - 0000: 00 00 00 4b 7c 4a 5b 6b - 0008: ab c4 5a b1 c1 37 6a 46 - 0010: 81 fb 8b 72 11 33 9d 26 - 0018: 41 ba ba 9a 23 88 48 34 - 0020: 27 da d2 01 08 8b f0 7c - 0028: b3 fe 2a 1f f1 8f 52 ac - 0030: 94 6a b7 aa 99 3a 9a 26 - 0038: da 9d 7f c5 70 f4 a2 c7 - 0040: 38 29 fe 6b 1c 87 c3 17 - 0048: 6b e8 f7 66 35 c9 c1 + 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) diff --git a/tests/snapshots/bootstrap_snapshot__string_payload.snap b/tests/snapshots/bootstrap_snapshot__string_payload.snap index 2647f0f2d..8656572e4 100644 --- a/tests/snapshots/bootstrap_snapshot__string_payload.snap +++ b/tests/snapshots/bootstrap_snapshot__string_payload.snap @@ -16,16 +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 0x7c: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 - child 0x9a: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 listing frame: 54 bytes - 0000: 00 00 00 32 7c 4a 5b 6b - 0008: ab c4 5a b1 c1 37 6a 46 - 0010: 81 fb 8b 72 11 33 9d 26 - 0018: 41 ba ba 9a 23 9a 26 da - 0020: 9d 7f c5 70 f4 a2 c7 38 - 0028: 29 fe 6b 1c 87 c3 17 6b - 0030: e8 f7 66 35 c9 c1 + 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) diff --git a/tests/snapshots/bootstrap_snapshot__v1_populated_provider.snap b/tests/snapshots/bootstrap_snapshot__v1_populated_provider.snap index 0b0a19331..3d10e73f6 100644 --- a/tests/snapshots/bootstrap_snapshot__v1_populated_provider.snap +++ b/tests/snapshots/bootstrap_snapshot__v1_populated_provider.snap @@ -21,17 +21,17 @@ received 14 bytes │ received 6 bytes 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: 7c 4a 5b 6b ab c4 5a b1 │ 0000: 00 00 00 4f 03 00 00 00 - 0010: c1 37 6a 46 81 fb 8b 72 │ 0008: 7c 4a 5b 6b ab c4 5a b1 - 0018: 11 33 9d 26 41 ba ba 9a │ 0010: c1 37 6a 46 81 fb 8b 72 - 0020: 23 88 48 34 27 da d2 01 │ 0018: 11 33 9d 26 41 ba ba 9a - 0028: 08 8b f0 7c b3 fe 2a 1f │ 0020: 23 88 48 34 27 da d2 01 - 0030: f1 8f 52 ac 94 6a b7 aa │ 0028: 08 8b f0 7c b3 fe 2a 1f - 0038: 99 3a 9a 26 da 9d 7f c5 │ 0030: f1 8f 52 ac 94 6a b7 aa - 0040: 70 f4 a2 c7 38 29 fe 6b │ 0038: 99 3a 9a 26 da 9d 7f c5 - 0048: 1c 87 c3 17 6b e8 f7 66 │ 0040: 70 f4 a2 c7 38 29 fe 6b - 0050: 35 c9 c1 │ 0048: 1c 87 c3 17 6b e8 f7 66 -received 19 bytes │ 0050: 35 c9 c1 + 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 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 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 46399a8f7..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 @@ -17,12 +17,12 @@ version frame: 34 bytes 0018: 00 00 00 00 40 0f ff 00 0020: 1f f9 listing: 1 child(ren) - child 0x79: 97fdeb2f910d20fbe119f2671eb624cf9c8417143a2435d4 + child 0x79: 5f06438950116f9d1463d15d1c3b1dadd5e17dcd5e244f29 listing frame: 29 bytes - 0000: 00 00 00 19 79 97 fd eb - 0008: 2f 91 0d 20 fb e1 19 f2 - 0010: 67 1e b6 24 cf 9c 84 17 - 0018: 14 3a 24 35 d4 + 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) diff --git a/tests/snapshots/gossip_snapshot__batched_supply_run.snap b/tests/snapshots/gossip_snapshot__batched_supply_run.snap index 219ece854..ad1e0f7ac 100644 --- a/tests/snapshots/gossip_snapshot__batched_supply_run.snap +++ b/tests/snapshots/gossip_snapshot__batched_supply_run.snap @@ -17,12 +17,12 @@ version frame: 34 bytes 0018: 00 00 00 00 40 0f ff 00 0020: 1f f9 listing: 1 child(ren) - child 0x79: 97fdeb2f910d20fbe119f2671eb624cf9c8417143a2435d4 + child 0x79: 5f06438950116f9d1463d15d1c3b1dadd5e17dcd5e244f29 listing frame: 29 bytes - 0000: 00 00 00 19 79 97 fd eb - 0008: 2f 91 0d 20 fb e1 19 f2 - 0010: 67 1e b6 24 cf 9c 84 17 - 0018: 14 3a 24 35 d4 + 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) diff --git a/tests/snapshots/gossip_snapshot__both_redact_the_same_message.snap b/tests/snapshots/gossip_snapshot__both_redact_the_same_message.snap index 6f94e1006..e9d20c958 100644 --- a/tests/snapshots/gossip_snapshot__both_redact_the_same_message.snap +++ b/tests/snapshots/gossip_snapshot__both_redact_the_same_message.snap @@ -16,12 +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 0x7c: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa listing frame: 29 bytes - 0000: 00 00 00 19 7c 4a 5b 6b - 0008: ab c4 5a b1 c1 37 6a 46 - 0010: 81 fb 8b 72 11 33 9d 26 - 0018: 41 ba ba 9a 23 + 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 @@ -44,11 +44,11 @@ version frame: 30 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 5d c0 listing: 1 child(ren) - child 0x7c: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa listing frame: 29 bytes - 0000: 00 00 00 19 7c 4a 5b 6b - 0008: ab c4 5a b1 c1 37 6a 46 - 0010: 81 fb 8b 72 11 33 9d 26 - 0018: 41 ba ba 9a 23 + 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 1466cebd0..c240c319b 100644 --- a/tests/snapshots/gossip_snapshot__bulk_initiator_ships_opening_supplies.snap +++ b/tests/snapshots/gossip_snapshot__bulk_initiator_ships_opening_supplies.snap @@ -16,12 +16,12 @@ version frame: 32 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 40 ff 01 f9 listing: 1 child(ren) - child 0x1b: 176df43d3144011ce7429832cad9212cf152a4d343757e68 + child 0x1b: 4c9f80ab8bdeed98a72d4976c2879ccd5cb17e2aeacd24d3 listing frame: 29 bytes - 0000: 00 00 00 19 1b 17 6d f4 - 0008: 3d 31 44 01 1c e7 42 98 - 0010: 32 ca d9 21 2c f1 52 a4 - 0018: d3 43 75 7e 68 + 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) @@ -54,20 +54,20 @@ version frame: 30 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 70 77 listing: 3 child(ren) - child 0x1a: f03024f7dbe56599296b2eb532822f29be44c48777943703 - child 0x36: 9ddf089f8c85ffebaf8a346d1af381f138082ce95f276640 - child 0xf2: 9b77e29e06b3bb072170d1851eb26c6007fd01cbd5c9a2f7 + child 0x1a: f1f3aa8a784d1361fc809d57df14a1955f8956c55e120e19 + child 0x36: a93c14a59c62f37321d21557b0e821de471a7e3d079c6fce + child 0xf2: 42bb8c7ce00cc0681c05edde50af1bd47d5d94d0a136c882 listing frame: 79 bytes - 0000: 00 00 00 4b 1a f0 30 24 - 0008: f7 db e5 65 99 29 6b 2e - 0010: b5 32 82 2f 29 be 44 c4 - 0018: 87 77 94 37 03 36 9d df - 0020: 08 9f 8c 85 ff eb af 8a - 0028: 34 6d 1a f3 81 f1 38 08 - 0030: 2c e9 5f 27 66 40 f2 9b - 0038: 77 e2 9e 06 b3 bb 07 21 - 0040: 70 d1 85 1e b2 6c 60 07 - 0048: fd 01 cb d5 c9 a2 f7 + 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) diff --git a/tests/snapshots/gossip_snapshot__converged_forks_noop.snap b/tests/snapshots/gossip_snapshot__converged_forks_noop.snap index 009683b63..efa28d7bc 100644 --- a/tests/snapshots/gossip_snapshot__converged_forks_noop.snap +++ b/tests/snapshots/gossip_snapshot__converged_forks_noop.snap @@ -16,16 +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 0x7c: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 - child 0x9a: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 listing frame: 54 bytes - 0000: 00 00 00 32 7c 4a 5b 6b - 0008: ab c4 5a b1 c1 37 6a 46 - 0010: 81 fb 8b 72 11 33 9d 26 - 0018: 41 ba ba 9a 23 9a 26 da - 0020: 9d 7f c5 70 f4 a2 c7 38 - 0028: 29 fe 6b 1c 87 c3 17 6b - 0030: e8 f7 66 35 c9 c1 + 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 @@ -43,15 +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 0x7c: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 - child 0x9a: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 listing frame: 54 bytes - 0000: 00 00 00 32 7c 4a 5b 6b - 0008: ab c4 5a b1 c1 37 6a 46 - 0010: 81 fb 8b 72 11 33 9d 26 - 0018: 41 ba ba 9a 23 9a 26 da - 0020: 9d 7f c5 70 f4 a2 c7 38 - 0028: 29 fe 6b 1c 87 c3 17 6b - 0030: e8 f7 66 35 c9 c1 + 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 1fa387d21..d9ef1cc65 100644 --- a/tests/snapshots/gossip_snapshot__deep_trie_divergence.snap +++ b/tests/snapshots/gossip_snapshot__deep_trie_divergence.snap @@ -16,74 +16,74 @@ 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 0x4: 9706f21cc160f366a0857b586b863c58a39f11d2864d7c0c - child 0x1b: a6849e73f4415befd030b3a2534e3dfc044e651c5c871f9a - child 0x21: e0bac08691f3a1bd81a3b9922407177bf6444cfaa4dee952 - child 0x4b: 91e9ec619218a54fb52023da657470cd6fc3cc437cb64bf7 - child 0x66: 08b51b7fd019850239388c3cc11f68902d350b46651bd793 - child 0x6c: 76aed85cfc9f76a4ad7110dce5cdc3a9fe31aa393f0508ac - child 0x6f: f563da5d17469396480e9d82ddde7a0154a9d8b471d4f029 - child 0x87: fa2cae4ddab64d8a20951bac33c328218ed7fb4a25de4af5 - child 0x93: 00b9252e54561ed3e303a1413e6f1d8b1115bd578a905323 - child 0xa0: cb4eca4fd3dc50fb7ac7f18c4acaa47b9802fff0dbf72a08 - child 0xb4: 7697b4614ab59c3954130a7a358dc8b49038a565c3e7b488 - child 0xb7: d0584bc370a49dafed3633b6232b07e71014dc89a92fb110 - child 0xc4: abda8abdc1256b7a24333e73af3f71de68a8331f368d3cae - child 0xd0: 0d61af285f22355ed1e9cd8481f53092ea95de4dfdc56e42 - child 0xf3: e35b9d171bc5412736bbec70ba5cc93ee5e4516ce8b125d1 - child 0xf5: d92b43bfe7eb0b6b56c71181959b9e8881f80e6c2198adf7 + 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 97 06 f2 - 0008: 1c c1 60 f3 66 a0 85 7b - 0010: 58 6b 86 3c 58 a3 9f 11 - 0018: d2 86 4d 7c 0c 1b a6 84 - 0020: 9e 73 f4 41 5b ef d0 30 - 0028: b3 a2 53 4e 3d fc 04 4e - 0030: 65 1c 5c 87 1f 9a 21 e0 - 0038: ba c0 86 91 f3 a1 bd 81 - 0040: a3 b9 92 24 07 17 7b f6 - 0048: 44 4c fa a4 de e9 52 4b - 0050: 91 e9 ec 61 92 18 a5 4f - 0058: b5 20 23 da 65 74 70 cd - 0060: 6f c3 cc 43 7c b6 4b f7 - 0068: 66 08 b5 1b 7f d0 19 85 - 0070: 02 39 38 8c 3c c1 1f 68 - 0078: 90 2d 35 0b 46 65 1b d7 - 0080: 93 6c 76 ae d8 5c fc 9f - 0088: 76 a4 ad 71 10 dc e5 cd - 0090: c3 a9 fe 31 aa 39 3f 05 - 0098: 08 ac 6f f5 63 da 5d 17 - 00a0: 46 93 96 48 0e 9d 82 dd - 00a8: de 7a 01 54 a9 d8 b4 71 - 00b0: d4 f0 29 87 fa 2c ae 4d - 00b8: da b6 4d 8a 20 95 1b ac - 00c0: 33 c3 28 21 8e d7 fb 4a - 00c8: 25 de 4a f5 93 00 b9 25 - 00d0: 2e 54 56 1e d3 e3 03 a1 - 00d8: 41 3e 6f 1d 8b 11 15 bd - 00e0: 57 8a 90 53 23 a0 cb 4e - 00e8: ca 4f d3 dc 50 fb 7a c7 - 00f0: f1 8c 4a ca a4 7b 98 02 - 00f8: ff f0 db f7 2a 08 b4 76 - 0100: 97 b4 61 4a b5 9c 39 54 - 0108: 13 0a 7a 35 8d c8 b4 90 - 0110: 38 a5 65 c3 e7 b4 88 b7 - 0118: d0 58 4b c3 70 a4 9d af - 0120: ed 36 33 b6 23 2b 07 e7 - 0128: 10 14 dc 89 a9 2f b1 10 - 0130: c4 ab da 8a bd c1 25 6b - 0138: 7a 24 33 3e 73 af 3f 71 - 0140: de 68 a8 33 1f 36 8d 3c - 0148: ae d0 0d 61 af 28 5f 22 - 0150: 35 5e d1 e9 cd 84 81 f5 - 0158: 30 92 ea 95 de 4d fd c5 - 0160: 6e 42 f3 e3 5b 9d 17 1b - 0168: c5 41 27 36 bb ec 70 ba - 0170: 5c c9 3e e5 e4 51 6c e8 - 0178: b1 25 d1 f5 d9 2b 43 bf - 0180: e7 eb 0b 6b 56 c7 11 81 - 0188: 95 9b 9e 88 81 f8 0e 6c - 0190: 21 98 ad f7 + 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: Supply(Continue) supply run: 1 record(s) @@ -214,70 +214,70 @@ version frame: 30 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 70 43 listing: 15 child(ren) - child 0x1a: f03024f7dbe56599296b2eb532822f29be44c48777943703 - child 0x1e: 146bd6081df473f2416d0bde6d5f02c98e3b716094270b0f - child 0x36: 9ddf089f8c85ffebaf8a346d1af381f138082ce95f276640 - child 0x47: 43189a95d8130f683dbf72f4dd6699fd6f44ee56472a967e - child 0x5b: 2515879992e8c053e0ad7818be3a3a1100a77fdfcfc59857 - child 0x5d: 88eb18314907a0f9a42ab7bfa0ee94335a57a9732f427f17 - child 0x5e: 67f39e1d23f94021cd8718f9ba8b1c0826de3a384963189f - child 0x74: 8deb535ac2e690d1254c5982dfd6176f3e55263b82b80028 - child 0x83: 89c2ca0a6098161085a6fb172f4a5898a03fe5d7d7c79d88 - child 0x9b: 897bbd87fe13d0ccc702570c2cd7a8cb8a689a87c530f2eb - child 0xc0: 1f47039fd08e6128872926842a157b7499599ff4ced098ab - child 0xc6: ab8d77b591864027d16445c97070bfdff2809d812393640d - child 0xe9: 28b444ce804288129d2ccafeef605842e9e978f1e475ddd1 - child 0xf2: 9b77e29e06b3bb072170d1851eb26c6007fd01cbd5c9a2f7 - child 0xf8: 309d1bc3b1291b1e1c2b83f34d2dfa13f14083c9b1032da6 + 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 f0 30 24 - 0008: f7 db e5 65 99 29 6b 2e - 0010: b5 32 82 2f 29 be 44 c4 - 0018: 87 77 94 37 03 1e 14 6b - 0020: d6 08 1d f4 73 f2 41 6d - 0028: 0b de 6d 5f 02 c9 8e 3b - 0030: 71 60 94 27 0b 0f 36 9d - 0038: df 08 9f 8c 85 ff eb af - 0040: 8a 34 6d 1a f3 81 f1 38 - 0048: 08 2c e9 5f 27 66 40 47 - 0050: 43 18 9a 95 d8 13 0f 68 - 0058: 3d bf 72 f4 dd 66 99 fd - 0060: 6f 44 ee 56 47 2a 96 7e - 0068: 5b 25 15 87 99 92 e8 c0 - 0070: 53 e0 ad 78 18 be 3a 3a - 0078: 11 00 a7 7f df cf c5 98 - 0080: 57 5d 88 eb 18 31 49 07 - 0088: a0 f9 a4 2a b7 bf a0 ee - 0090: 94 33 5a 57 a9 73 2f 42 - 0098: 7f 17 5e 67 f3 9e 1d 23 - 00a0: f9 40 21 cd 87 18 f9 ba - 00a8: 8b 1c 08 26 de 3a 38 49 - 00b0: 63 18 9f 74 8d eb 53 5a - 00b8: c2 e6 90 d1 25 4c 59 82 - 00c0: df d6 17 6f 3e 55 26 3b - 00c8: 82 b8 00 28 83 89 c2 ca - 00d0: 0a 60 98 16 10 85 a6 fb - 00d8: 17 2f 4a 58 98 a0 3f e5 - 00e0: d7 d7 c7 9d 88 9b 89 7b - 00e8: bd 87 fe 13 d0 cc c7 02 - 00f0: 57 0c 2c d7 a8 cb 8a 68 - 00f8: 9a 87 c5 30 f2 eb c0 1f - 0100: 47 03 9f d0 8e 61 28 87 - 0108: 29 26 84 2a 15 7b 74 99 - 0110: 59 9f f4 ce d0 98 ab c6 - 0118: ab 8d 77 b5 91 86 40 27 - 0120: d1 64 45 c9 70 70 bf df - 0128: f2 80 9d 81 23 93 64 0d - 0130: e9 28 b4 44 ce 80 42 88 - 0138: 12 9d 2c ca fe ef 60 58 - 0140: 42 e9 e9 78 f1 e4 75 dd - 0148: d1 f2 9b 77 e2 9e 06 b3 - 0150: bb 07 21 70 d1 85 1e b2 - 0158: 6c 60 07 fd 01 cb d5 c9 - 0160: a2 f7 f8 30 9d 1b c3 b1 - 0168: 29 1b 1e 1c 2b 83 f3 4d - 0170: 2d fa 13 f1 40 83 c9 b1 - 0178: 03 2d a6 + 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) diff --git a/tests/snapshots/gossip_snapshot__early_supplies_honor_redactions.snap b/tests/snapshots/gossip_snapshot__early_supplies_honor_redactions.snap index 029a5fbae..24f515dd8 100644 --- a/tests/snapshots/gossip_snapshot__early_supplies_honor_redactions.snap +++ b/tests/snapshots/gossip_snapshot__early_supplies_honor_redactions.snap @@ -17,12 +17,12 @@ version frame: 35 bytes 0018: 00 00 00 00 40 02 00 30 0020: 00 ff f4 listing: 1 child(ren) - child 0x9a: a04d4d00cd54506f37c22d58d5321e135fdc27778707128e + child 0x9a: 636d485e0ec23826ab88a639e80fe993d036c716de1441a1 listing frame: 29 bytes - 0000: 00 00 00 19 9a a0 4d 4d - 0008: 00 cd 54 50 6f 37 c2 2d - 0010: 58 d5 32 1e 13 5f dc 27 - 0018: 77 87 07 12 8e + 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) @@ -54,20 +54,20 @@ version frame: 31 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 54 1e c0 listing: 3 child(ren) - child 0x90: d4cf592c487d3a67dd368e1a0c450b8c45088d07aae68a9a - child 0xab: bfc70672bfd0ac57279fb33afa6234203b54a984e2a63eb0 - child 0xfa: a3d9effce8b2ffb768b9ea4291a7068171652e7199da7f5c + child 0x90: 937506250024599968f64f7c4e2f3ac050a148a65441140d + child 0xab: 0e0630bedb15ab6e5a395048d02bb03e950f377b95eed0b1 + child 0xfa: 096df9d6e522813481d0f18c5e7cb824eb404882002c26a2 listing frame: 79 bytes - 0000: 00 00 00 4b 90 d4 cf 59 - 0008: 2c 48 7d 3a 67 dd 36 8e - 0010: 1a 0c 45 0b 8c 45 08 8d - 0018: 07 aa e6 8a 9a ab bf c7 - 0020: 06 72 bf d0 ac 57 27 9f - 0028: b3 3a fa 62 34 20 3b 54 - 0030: a9 84 e2 a6 3e b0 fa a3 - 0038: d9 ef fc e8 b2 ff b7 68 - 0040: b9 ea 42 91 a7 06 81 71 - 0048: 65 2e 71 99 da 7f 5c + 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: Supply(Continue) supply run: 1 record(s) diff --git a/tests/snapshots/gossip_snapshot__fork_insert_redact.snap b/tests/snapshots/gossip_snapshot__fork_insert_redact.snap index 2148f4e24..e6311ca7c 100644 --- a/tests/snapshots/gossip_snapshot__fork_insert_redact.snap +++ b/tests/snapshots/gossip_snapshot__fork_insert_redact.snap @@ -16,16 +16,16 @@ 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 0x64: 76c57e179acd80b1cd3d2f2db7364a23fac273a7ba43a53a - child 0x7c: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 + child 0x64: bb28d1c9e9fa40672e21164802bc8e7a23eb03126468e82c + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa listing frame: 54 bytes - 0000: 00 00 00 32 64 76 c5 7e - 0008: 17 9a cd 80 b1 cd 3d 2f - 0010: 2d b7 36 4a 23 fa c2 73 - 0018: a7 ba 43 a5 3a 7c 4a 5b - 0020: 6b ab c4 5a b1 c1 37 6a - 0028: 46 81 fb 8b 72 11 33 9d - 0030: 26 41 ba ba 9a 23 + 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: Supply(Continue) supply run: 1 record(s) @@ -55,16 +55,16 @@ 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 0x9a: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 - child 0xe7: 726751de5c398bfebd9dd3ad534412578f5784c45147a879 + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 + child 0xe7: c9b78024d3d0ad1a5523b4ac969d289fee0651305c416d72 listing frame: 54 bytes - 0000: 00 00 00 32 9a 26 da 9d - 0008: 7f c5 70 f4 a2 c7 38 29 - 0010: fe 6b 1c 87 c3 17 6b e8 - 0018: f7 66 35 c9 c1 e7 72 67 - 0020: 51 de 5c 39 8b fe bd 9d - 0028: d3 ad 53 44 12 57 8f 57 - 0030: 84 c4 51 47 a8 79 + 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) diff --git a/tests/snapshots/gossip_snapshot__one_sided_transfer.snap b/tests/snapshots/gossip_snapshot__one_sided_transfer.snap index d2109276a..b2c6a0d0c 100644 --- a/tests/snapshots/gossip_snapshot__one_sided_transfer.snap +++ b/tests/snapshots/gossip_snapshot__one_sided_transfer.snap @@ -16,16 +16,16 @@ 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 0x93: 00b9252e54561ed3e303a1413e6f1d8b1115bd578a905323 - child 0xa0: cb4eca4fd3dc50fb7ac7f18c4acaa47b9802fff0dbf72a08 + child 0x93: c4c0139f3f4f4afe9715d26a43953691cdc861574ffc8196 + child 0xa0: 7835b0a8791af76763e8248004d356e7d5c938bb893ecc9c listing frame: 54 bytes - 0000: 00 00 00 32 93 00 b9 25 - 0008: 2e 54 56 1e d3 e3 03 a1 - 0010: 41 3e 6f 1d 8b 11 15 bd - 0018: 57 8a 90 53 23 a0 cb 4e - 0020: ca 4f d3 dc 50 fb 7a c7 - 0028: f1 8c 4a ca a4 7b 98 02 - 0030: ff f0 db f7 2a 08 + 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) diff --git a/tests/snapshots/gossip_snapshot__redaction_only.snap b/tests/snapshots/gossip_snapshot__redaction_only.snap index dcb3bba7d..a3c62a93e 100644 --- a/tests/snapshots/gossip_snapshot__redaction_only.snap +++ b/tests/snapshots/gossip_snapshot__redaction_only.snap @@ -16,12 +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 0x7c: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa listing frame: 29 bytes - 0000: 00 00 00 19 7c 4a 5b 6b - 0008: ab c4 5a b1 c1 37 6a 46 - 0010: 81 fb 8b 72 11 33 9d 26 - 0018: 41 ba ba 9a 23 + 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 @@ -39,16 +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 0x7c: 4a5b6babc45ab1c1376a4681fb8b7211339d2641baba9a23 - child 0x9a: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 + child 0x7c: f53f9477309788c6ed8834008f394a3be5f212959fac55fa + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 listing frame: 54 bytes - 0000: 00 00 00 32 7c 4a 5b 6b - 0008: ab c4 5a b1 c1 37 6a 46 - 0010: 81 fb 8b 72 11 33 9d 26 - 0018: 41 ba ba 9a 23 9a 26 da - 0020: 9d 7f c5 70 f4 a2 c7 38 - 0028: 29 fe 6b 1c 87 c3 17 6b - 0030: e8 f7 66 35 c9 c1 + 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 bd42e9a0f..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,12 +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 0x9a: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 listing frame: 29 bytes - 0000: 00 00 00 19 9a 26 da 9d - 0008: 7f c5 70 f4 a2 c7 38 29 - 0010: fe 6b 1c 87 c3 17 6b e8 - 0018: f7 66 35 c9 c1 + 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 @@ -44,11 +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 0x9a: 26da9d7fc570f4a2c73829fe6b1c87c3176be8f76635c9c1 + child 0x9a: cca7fd1fe957c5267ce159c8f3fe95bc4f496f6b2fb5fa39 listing frame: 29 bytes - 0000: 00 00 00 19 9a 26 da 9d - 0008: 7f c5 70 f4 a2 c7 38 29 - 0010: fe 6b 1c 87 c3 17 6b e8 - 0018: f7 66 35 c9 c1 + 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 00d4d4077..6c31b259a 100644 --- a/tests/snapshots/gossip_snapshot__string_payload.snap +++ b/tests/snapshots/gossip_snapshot__string_payload.snap @@ -16,12 +16,12 @@ 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 0x93: 00b9252e54561ed3e303a1413e6f1d8b1115bd578a905323 + child 0x93: c4c0139f3f4f4afe9715d26a43953691cdc861574ffc8196 listing frame: 29 bytes - 0000: 00 00 00 19 93 00 b9 25 - 0008: 2e 54 56 1e d3 e3 03 a1 - 0010: 41 3e 6f 1d 8b 11 15 bd - 0018: 57 8a 90 53 23 + 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: Supply(Continue) supply run: 1 record(s) @@ -50,12 +50,12 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 77 listing: 1 child(ren) - child 0xf2: 9b77e29e06b3bb072170d1851eb26c6007fd01cbd5c9a2f7 + child 0xf2: 42bb8c7ce00cc0681c05edde50af1bd47d5d94d0a136c882 listing frame: 29 bytes - 0000: 00 00 00 19 f2 9b 77 e2 - 0008: 9e 06 b3 bb 07 21 70 d1 - 0010: 85 1e b2 6c 60 07 fd 01 - 0018: cb d5 c9 a2 f7 + 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) diff --git a/tests/snapshots/gossip_snapshot__v1_one_sided_transfer.snap b/tests/snapshots/gossip_snapshot__v1_one_sided_transfer.snap index 6161d0f2f..e639ef9ad 100644 --- a/tests/snapshots/gossip_snapshot__v1_one_sided_transfer.snap +++ b/tests/snapshots/gossip_snapshot__v1_one_sided_transfer.snap @@ -21,14 +21,14 @@ received 14 bytes │ received 7 bytes 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: 93 00 b9 25 2e 54 56 1e │ 0000: 00 00 00 36 02 00 00 00 - 0010: d3 e3 03 a1 41 3e 6f 1d │ 0008: 93 00 b9 25 2e 54 56 1e - 0018: 8b 11 15 bd 57 8a 90 53 │ 0010: d3 e3 03 a1 41 3e 6f 1d - 0020: 23 a0 cb 4e ca 4f d3 dc │ 0018: 8b 11 15 bd 57 8a 90 53 - 0028: 50 fb 7a c7 f1 8c 4a ca │ 0020: 23 a0 cb 4e ca 4f d3 dc - 0030: a4 7b 98 02 ff f0 db f7 │ 0028: 50 fb 7a c7 f1 8c 4a ca - 0038: 2a 08 │ 0030: a4 7b 98 02 ff f0 db f7 -received 18 bytes │ 0038: 2a 08 + 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 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 diff --git a/tests/snapshots/retire_snapshot__divergent_retire.snap b/tests/snapshots/retire_snapshot__divergent_retire.snap index 59aa8e157..7d04a4304 100644 --- a/tests/snapshots/retire_snapshot__divergent_retire.snap +++ b/tests/snapshots/retire_snapshot__divergent_retire.snap @@ -16,12 +16,12 @@ 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 0x93: 00b9252e54561ed3e303a1413e6f1d8b1115bd578a905323 + child 0x93: c4c0139f3f4f4afe9715d26a43953691cdc861574ffc8196 listing frame: 29 bytes - 0000: 00 00 00 19 93 00 b9 25 - 0008: 2e 54 56 1e d3 e3 03 a1 - 0010: 41 3e 6f 1d 8b 11 15 bd - 0018: 57 8a 90 53 23 + 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: Supply(Continue) supply run: 1 record(s) @@ -49,12 +49,12 @@ version frame: 29 bytes 0010: 00 00 00 00 00 02 19 00 0018: 00 00 00 00 77 listing: 1 child(ren) - child 0xf2: 9b77e29e06b3bb072170d1851eb26c6007fd01cbd5c9a2f7 + child 0xf2: 42bb8c7ce00cc0681c05edde50af1bd47d5d94d0a136c882 listing frame: 29 bytes - 0000: 00 00 00 19 f2 9b 77 e2 - 0008: 9e 06 b3 bb 07 21 70 d1 - 0010: 85 1e b2 6c 60 07 fd 01 - 0018: cb d5 c9 a2 f7 + 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) diff --git a/tests/snapshots/retire_snapshot__retire_into_bootstrapper.snap b/tests/snapshots/retire_snapshot__retire_into_bootstrapper.snap index 33e99cd38..0c1c7b55f 100644 --- a/tests/snapshots/retire_snapshot__retire_into_bootstrapper.snap +++ b/tests/snapshots/retire_snapshot__retire_into_bootstrapper.snap @@ -35,16 +35,16 @@ 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 0x36: 9ddf089f8c85ffebaf8a346d1af381f138082ce95f276640 - child 0xf2: 9b77e29e06b3bb072170d1851eb26c6007fd01cbd5c9a2f7 + child 0x36: a93c14a59c62f37321d21557b0e821de471a7e3d079c6fce + child 0xf2: 42bb8c7ce00cc0681c05edde50af1bd47d5d94d0a136c882 listing frame: 54 bytes - 0000: 00 00 00 32 36 9d df 08 - 0008: 9f 8c 85 ff eb af 8a 34 - 0010: 6d 1a f3 81 f1 38 08 2c - 0018: e9 5f 27 66 40 f2 9b 77 - 0020: e2 9e 06 b3 bb 07 21 70 - 0028: d1 85 1e b2 6c 60 07 fd - 0030: 01 cb d5 c9 a2 f7 + 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) diff --git a/tests/snapshots/retire_snapshot__v1_divergent_retire.snap b/tests/snapshots/retire_snapshot__v1_divergent_retire.snap index 0c927c02b..e979846e4 100644 --- a/tests/snapshots/retire_snapshot__v1_divergent_retire.snap +++ b/tests/snapshots/retire_snapshot__v1_divergent_retire.snap @@ -18,17 +18,17 @@ 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 a7 a6 │ sent 32 bytes - 0010: aa 34 c1 8b cb d1 59 9e │ 0000: 00 00 00 1c 01 00 00 00 - 0018: 58 54 81 6c f0 8b b3 1b │ 0008: a7 a6 aa 34 c1 8b cb d1 - 0020: 88 db a7 6e 7c 34 │ 0010: 59 9e 58 54 81 6c f0 8b -sent 33 bytes │ 0018: b3 1b 88 db a7 6e 7c 34 + 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: 93 00 b9 25 2e 54 56 1e │ 0000: 00 00 00 1d 01 00 00 00 - 0010: d3 e3 03 a1 41 3e 6f 1d │ 0008: 93 00 b9 25 2e 54 56 1e - 0018: 8b 11 15 bd 57 8a 90 53 │ 0010: d3 e3 03 a1 41 3e 6f 1d - 0020: 23 │ 0018: 8b 11 15 bd 57 8a 90 53 -received 54 bytes │ 0020: 23 + 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 From 3d16765f9145113c187f79f9892c7f9c7d0f72bb Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 19 Aug 2026 13:39:16 -0400 Subject: [PATCH 11/11] Update lib.rs --- src/lib.rs | 43 ++++++++++++------------------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2b17e5d04..ed5d45e39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -230,32 +230,17 @@ //! [`AsyncRead`](tokio::io::AsyncRead) and [`AsyncWrite`](tokio::io::AsyncWrite); //! no Tokio runtime, spawning, sockets, or timers are required by this crate. //! -//! # Message payloads +//! # Message payloads and compatibility //! //! Your message type `T` needs [`serde::Serialize`] and -//! [`serde::de::DeserializeOwned`]; payloads travel and are cached as +//! [`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 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 -//! [`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. +//! 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 //! @@ -264,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