feat(core): ack validation, custody release, confidence events
This commit is contained in:
@@ -32,3 +32,11 @@ pub const MAX_FRAME: usize = 1500;
|
|||||||
|
|
||||||
/// Maximum number of content-IDs an `Ack` receipt may carry.
|
/// Maximum number of content-IDs an `Ack` receipt may carry.
|
||||||
pub const ACK_IDS_CAP: usize = 90;
|
pub const ACK_IDS_CAP: usize = 90;
|
||||||
|
|
||||||
|
/// Cap on the `Exchange` `events` queue of `ConfidenceEvent`s (one per
|
||||||
|
/// broadcast id a peer has confirmed).
|
||||||
|
pub const EVENT_QUEUE_CAP: usize = 64;
|
||||||
|
|
||||||
|
/// Cap on the `Exchange` per-peer `bcast_sent` table of broadcast ids
|
||||||
|
/// dispatched to a peer.
|
||||||
|
pub const BCAST_SENT_CAP: usize = 256;
|
||||||
|
|||||||
+465
-29
@@ -39,10 +39,22 @@ pub struct InFlight {
|
|||||||
pub count: u8,
|
pub count: u8,
|
||||||
/// `age_ms` the stream was encoded with.
|
/// `age_ms` the stream was encoded with.
|
||||||
pub age_ms: u64,
|
pub age_ms: u64,
|
||||||
/// Length of the encoded observation the fragments are cut from.
|
/// Length of the encoded observation the fragments are cut from. (A
|
||||||
|
/// fragment cut from the wrong offset would reassemble to corrupt bytes.)
|
||||||
pub len: usize,
|
pub len: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emitted for a broadcast observation a peer has confirmed holding via an
|
||||||
|
/// `Ack` receipt: [`ConfidenceEvent::peer`] is who received it and `id` is
|
||||||
|
/// the broadcast's content-ID. Produced through `Exchange::drain_events`.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ConfidenceEvent {
|
||||||
|
/// The node id of the peer that acknowledged receiving the observation.
|
||||||
|
pub peer: NodeId,
|
||||||
|
/// The content-ID of the broadcast observation that was acknowledged.
|
||||||
|
pub id: ObservationId,
|
||||||
|
}
|
||||||
|
|
||||||
/// A single peer the [`Exchange`] is tracking.
|
/// A single peer the [`Exchange`] is tracking.
|
||||||
pub struct Peer<A> {
|
pub struct Peer<A> {
|
||||||
/// Wire address of the peer.
|
/// Wire address of the peer.
|
||||||
@@ -110,15 +122,24 @@ pub struct Exchange<A> {
|
|||||||
salt: u32,
|
salt: u32,
|
||||||
peers: Vec<Peer<A>, { consts::MAX_PEERS }>,
|
peers: Vec<Peer<A>, { consts::MAX_PEERS }>,
|
||||||
rr: usize,
|
rr: usize,
|
||||||
/// `(id, addr)` for each addressed message received from, or fully sent
|
/// `(id, addr, direction)` for each addressed message received from, or
|
||||||
/// to, `addr` during its current contact. [`Exchange::next_frame`] uses it
|
/// fully sent to, `addr` during its current contact. [`Exchange::next_frame`]
|
||||||
/// so an addressed message is offered to each peer at most once per contact
|
/// uses it so an addressed message is offered to each peer at most once per
|
||||||
/// and never bounced straight back to its source. Records for an address
|
/// contact and never bounced straight back to its source. Records for an
|
||||||
/// are dropped when its contact starts or ends. Bounded at
|
/// address are dropped when its contact starts or ends. Bounded at
|
||||||
/// `SEND_QUEUE_CAP`: on overflow the oldest record is evicted, so
|
/// `SEND_QUEUE_CAP`: on overflow the oldest record is evicted, so
|
||||||
/// suppression only weakens (ID dedup absorbs a repeat) and never fails
|
/// suppression only weakens (ID dedup absorbs a repeat) and never fails
|
||||||
/// closed on a `no_std` target.
|
/// closed on a `no_std` target. The `direction` (a `Sent` or `Received`)
|
||||||
bounce: Vec<(ObservationId, A), { consts::SEND_QUEUE_CAP }>,
|
/// lets a bounce only suppress an id the local node actually handed out to,
|
||||||
|
/// or received from, `addr` — never one merely observed while forwarding.
|
||||||
|
bounce: Vec<(ObservationId, A, BounceKind), { consts::SEND_QUEUE_CAP }>,
|
||||||
|
/// `(id, addr)` for each broadcast observation dispatched to `addr` on the
|
||||||
|
/// broadcast emit path. Used to validate inbound `Ack` receipts against
|
||||||
|
/// broadcasts we actually sent; overflow drops the oldest record.
|
||||||
|
bcast_sent: Vec<(ObservationId, A), { consts::BCAST_SENT_CAP }>,
|
||||||
|
/// One [`ConfidenceEvent`] per broadcast id a peer has confirmed, pushed
|
||||||
|
/// drop-oldest when full. Drain through [`Exchange::drain_events`].
|
||||||
|
events: Vec<ConfidenceEvent, { consts::EVENT_QUEUE_CAP }>,
|
||||||
/// How to undo the last frame [`Exchange::next_frame`] produced.
|
/// How to undo the last frame [`Exchange::next_frame`] produced.
|
||||||
undo: Option<Undo<A>>,
|
undo: Option<Undo<A>>,
|
||||||
}
|
}
|
||||||
@@ -132,6 +153,8 @@ impl<A: Clone + PartialEq + core::fmt::Debug> Exchange<A> {
|
|||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
rr: 0,
|
rr: 0,
|
||||||
bounce: Vec::new(),
|
bounce: Vec::new(),
|
||||||
|
bcast_sent: Vec::new(),
|
||||||
|
events: Vec::new(),
|
||||||
undo: None,
|
undo: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,7 +275,7 @@ impl<A: Clone + PartialEq + core::fmt::Debug> Exchange<A> {
|
|||||||
if let Some(pos) = self
|
if let Some(pos) = self
|
||||||
.bounce
|
.bounce
|
||||||
.iter()
|
.iter()
|
||||||
.position(|(b, a)| *b == id && *a == undo.addr)
|
.position(|(b, a, _)| *b == id && *a == undo.addr)
|
||||||
{
|
{
|
||||||
self.bounce.remove(pos);
|
self.bounce.remove(pos);
|
||||||
}
|
}
|
||||||
@@ -322,10 +345,12 @@ impl<A: Clone + PartialEq + core::fmt::Debug> Exchange<A> {
|
|||||||
now: Tick,
|
now: Tick,
|
||||||
out: &mut [u8],
|
out: &mut [u8],
|
||||||
) -> Option<(A, usize)> {
|
) -> Option<(A, usize)> {
|
||||||
|
let addr = self.peers[idx].addr.clone();
|
||||||
loop {
|
loop {
|
||||||
let front = *self.peers[idx].send_queue.first()?;
|
let front = *self.peers[idx].send_queue.first()?;
|
||||||
if let Some(obs) = store.get(&front, now) {
|
if let Some(obs) = store.get(&front, now) {
|
||||||
if let Some(res) = self.emit(idx, front, &obs, None, mtu, out) {
|
if let Some(res) = self.emit(idx, front, &obs, None, mtu, out) {
|
||||||
|
self.record_bcast_sent(front, addr.clone());
|
||||||
return Some(res);
|
return Some(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,6 +358,18 @@ impl<A: Clone + PartialEq + core::fmt::Debug> Exchange<A> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Records that a broadcast for `id` was dispatched to `addr`, evicting the
|
||||||
|
/// oldest record when `bcast_sent` is full.
|
||||||
|
fn record_bcast_sent(&mut self, id: ObservationId, addr: A) {
|
||||||
|
if bcast_sent_contains(&self.bcast_sent, id, &addr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.bcast_sent.is_full() {
|
||||||
|
self.bcast_sent.remove(0);
|
||||||
|
}
|
||||||
|
let _ = self.bcast_sent.push((id, addr));
|
||||||
|
}
|
||||||
|
|
||||||
/// Addressed store-and-forward for the peer at `idx`: start sending one
|
/// Addressed store-and-forward for the peer at `idx`: start sending one
|
||||||
/// addressed observation, preferring *delivery* (`to == peer.id`) over
|
/// addressed observation, preferring *delivery* (`to == peer.id`) over
|
||||||
/// *epidemic* forwarding (`to == Some(other != me)`).
|
/// *epidemic* forwarding (`to == Some(other != me)`).
|
||||||
@@ -410,7 +447,9 @@ impl<A: Clone + PartialEq + core::fmt::Debug> Exchange<A> {
|
|||||||
peer.send_queue.remove(pos);
|
peer.send_queue.remove(pos);
|
||||||
undo.requeue = Some((pos, id));
|
undo.requeue = Some((pos, id));
|
||||||
}
|
}
|
||||||
if obs.to.is_some() && record_bounce(&mut self.bounce, id, addr.clone()) {
|
if obs.to.is_some()
|
||||||
|
&& record_bounce(&mut self.bounce, id, addr.clone(), BounceKind::Sent)
|
||||||
|
{
|
||||||
undo.bounce = Some(id);
|
undo.bounce = Some(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,6 +462,22 @@ impl<A: Clone + PartialEq + core::fmt::Debug> Exchange<A> {
|
|||||||
&self.peers
|
&self.peers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`Exchange`]s emit [`ConfidenceEvent`]s through [`Exchange::drain_events`];
|
||||||
|
/// `tines` surfaces them to its transport, so the drain method is unused
|
||||||
|
/// inside `tines-core` itself.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn drain_events(&mut self) -> Vec<ConfidenceEvent, { consts::EVENT_QUEUE_CAP }> {
|
||||||
|
let mut drained: Vec<ConfidenceEvent, { consts::EVENT_QUEUE_CAP }> = Vec::new();
|
||||||
|
while !self.events.is_empty() {
|
||||||
|
if drained.is_full() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let ev = self.events.remove(0);
|
||||||
|
let _ = drained.push(ev);
|
||||||
|
}
|
||||||
|
drained
|
||||||
|
}
|
||||||
|
|
||||||
fn add_peer(&mut self, addr: A) {
|
fn add_peer(&mut self, addr: A) {
|
||||||
if self.peers.iter().any(|p| p.addr == addr) {
|
if self.peers.iter().any(|p| p.addr == addr) {
|
||||||
return;
|
return;
|
||||||
@@ -442,7 +497,7 @@ impl<A: Clone + PartialEq + core::fmt::Debug> Exchange<A> {
|
|||||||
|
|
||||||
/// Drops the bounce records for `addr`: suppression lasts one contact.
|
/// Drops the bounce records for `addr`: suppression lasts one contact.
|
||||||
fn forget_bounces(&mut self, addr: &A) {
|
fn forget_bounces(&mut self, addr: &A) {
|
||||||
self.bounce.retain(|(_, a)| a != addr);
|
self.bounce.retain(|(_, baddr, _)| baddr != addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the index of the peer at `addr`, adding it as an implicit
|
/// Returns the index of the peer at `addr`, adding it as an implicit
|
||||||
@@ -492,13 +547,46 @@ impl<A: Clone + PartialEq + core::fmt::Debug> Exchange<A> {
|
|||||||
let obs = peer.reassembler.feed(id, index, count, &data, now)?;
|
let obs = peer.reassembler.feed(id, index, count, &data, now)?;
|
||||||
self.accept(obs, addr, store, now)
|
self.accept(obs, addr, store, now)
|
||||||
}
|
}
|
||||||
// Ack carries a handoff receipt of content-IDs; it produces no
|
Frame::Ack { from, ids } => self.on_ack(addr, from, ids, store, now),
|
||||||
// Observation here (ack handling in the exchange layer is out of
|
|
||||||
// scope for this wire-frame task).
|
|
||||||
Frame::Ack { .. } => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles an inbound `Ack` receipt from the peer at `addr`, whose node id
|
||||||
|
/// is `from`.
|
||||||
|
///
|
||||||
|
/// For each id in the receipt: an id the local node `Sent` to `addr` (per
|
||||||
|
/// the bounce table) is released from custody so it is never forwarded
|
||||||
|
/// again; an id the node dispatched as a broadcast to `addr` (per
|
||||||
|
/// `bcast_sent`) fires a [`ConfidenceEvent`]. A receipt for an id the node
|
||||||
|
/// never dispatched to `addr` is silently ignored. No path panics on
|
||||||
|
/// malformed input — an oversized id list just drops the overflow.
|
||||||
|
fn on_ack<S: Store>(
|
||||||
|
&mut self,
|
||||||
|
addr: A,
|
||||||
|
from: NodeId,
|
||||||
|
ids: Vec<ObservationId, { consts::ACK_IDS_CAP }>,
|
||||||
|
store: &mut S,
|
||||||
|
now: Tick,
|
||||||
|
) -> Option<Observation> {
|
||||||
|
for id in ids.iter() {
|
||||||
|
if was_sent(&self.bounce, *id, &addr) {
|
||||||
|
// Addressed: release custody for the local copy if still held
|
||||||
|
// and still addressed (a broadcast can never match `was_sent`).
|
||||||
|
let held_addressed = store.get(id, now).is_some_and(|obs| obs.to.is_some());
|
||||||
|
if held_addressed {
|
||||||
|
store.release(id);
|
||||||
|
}
|
||||||
|
} else if bcast_sent_contains(&self.bcast_sent, *id, &addr) {
|
||||||
|
// Broadcast: the peer confirms it holds our broadcast.
|
||||||
|
let _ = self.events.push(ConfidenceEvent {
|
||||||
|
peer: from,
|
||||||
|
id: *id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Stores an observation received from `addr`, returning it if it was new.
|
/// Stores an observation received from `addr`, returning it if it was new.
|
||||||
///
|
///
|
||||||
/// An addressed message is recorded against `addr` whether it was new or
|
/// An addressed message is recorded against `addr` whether it was new or
|
||||||
@@ -519,7 +607,7 @@ impl<A: Clone + PartialEq + core::fmt::Debug> Exchange<A> {
|
|||||||
let returned = obs.clone();
|
let returned = obs.clone();
|
||||||
let res = store.insert(obs, now);
|
let res = store.insert(obs, now);
|
||||||
if addressed && res.is_ok() {
|
if addressed && res.is_ok() {
|
||||||
record_bounce(&mut self.bounce, id, addr);
|
record_bounce(&mut self.bounce, id, addr, BounceKind::Received);
|
||||||
}
|
}
|
||||||
match res {
|
match res {
|
||||||
Ok(true) => Some(returned),
|
Ok(true) => Some(returned),
|
||||||
@@ -581,7 +669,7 @@ fn reject(obs: &Observation) -> bool {
|
|||||||
fn select_addressed<S: Store, A: PartialEq>(
|
fn select_addressed<S: Store, A: PartialEq>(
|
||||||
store: &S,
|
store: &S,
|
||||||
now: Tick,
|
now: Tick,
|
||||||
bounce: &[(ObservationId, A)],
|
bounce: &[(ObservationId, A, BounceKind)],
|
||||||
addr: &A,
|
addr: &A,
|
||||||
after: Option<ObservationId>,
|
after: Option<ObservationId>,
|
||||||
mut predicate: impl FnMut(&ObservationId, &Observation) -> bool,
|
mut predicate: impl FnMut(&ObservationId, &Observation) -> bool,
|
||||||
@@ -604,7 +692,7 @@ fn select_addressed<S: Store, A: PartialEq>(
|
|||||||
let Some(obs) = store.get(id, now) else {
|
let Some(obs) = store.get(id, now) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if obs.to.is_none() || reject(&obs) || !predicate(id, &obs) {
|
if obs.to.is_none() || reject(&obs) || store.is_released(id) || !predicate(id, &obs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if is_ahead {
|
if is_ahead {
|
||||||
@@ -616,21 +704,50 @@ fn select_addressed<S: Store, A: PartialEq>(
|
|||||||
ahead.or(wrapped)
|
ahead.or(wrapped)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` when `(id, addr)` has been recorded in the bounce table —
|
/// The direction that produced a bounce record: the local node `Sent` the id
|
||||||
/// i.e. `id` was received from, or sent to, `addr` this contact. Offering it
|
/// to `addr`, or `Received` it from `addr`. Used to make bounce suppression
|
||||||
/// to that peer again would only bounce, so it is skipped.
|
/// direction-aware — a `Sent` record can only release custody, a `Received`
|
||||||
fn is_sourced<A: PartialEq>(bounce: &[(ObservationId, A)], id: ObservationId, addr: &A) -> bool {
|
/// record can only suppress re-offering.
|
||||||
bounce
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
.iter()
|
enum BounceKind {
|
||||||
.any(|(bid, baddr)| *bid == id && baddr == addr)
|
Sent,
|
||||||
|
Received,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Records `(id, addr)` in the bounce table, evicting the oldest record when
|
/// Returns `true` when `(id, addr)` has been recorded in the bounce table —
|
||||||
/// full. Returns `true` if a new record was added.
|
/// i.e. `id` was received from, or sent to, `addr` this contact under either
|
||||||
|
/// direction. Offering it to that peer again would only bounce, so it is
|
||||||
|
/// skipped.
|
||||||
|
fn is_sourced<A: PartialEq>(
|
||||||
|
bounce: &[(ObservationId, A, BounceKind)],
|
||||||
|
id: ObservationId,
|
||||||
|
addr: &A,
|
||||||
|
) -> bool {
|
||||||
|
bounce
|
||||||
|
.iter()
|
||||||
|
.any(|(bid, baddr, _kind)| *bid == id && baddr == addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `id` was `Sent` to `addr`. An inbound `Ack` from `addr` may only
|
||||||
|
/// release custody of an id the local node actually dispatched to it — never
|
||||||
|
/// an id it only received, or merely observed while forwarding.
|
||||||
|
fn was_sent<A: PartialEq>(
|
||||||
|
bounce: &[(ObservationId, A, BounceKind)],
|
||||||
|
id: ObservationId,
|
||||||
|
addr: &A,
|
||||||
|
) -> bool {
|
||||||
|
bounce
|
||||||
|
.iter()
|
||||||
|
.any(|(bid, baddr, kind)| *bid == id && baddr == addr && *kind == BounceKind::Sent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records an `(id, addr, direction)` bounce in the table, evicting the oldest
|
||||||
|
/// record when full. Returns `true` if a new record was added.
|
||||||
fn record_bounce<A: PartialEq>(
|
fn record_bounce<A: PartialEq>(
|
||||||
bounce: &mut Vec<(ObservationId, A), { consts::SEND_QUEUE_CAP }>,
|
bounce: &mut Vec<(ObservationId, A, BounceKind), { consts::SEND_QUEUE_CAP }>,
|
||||||
id: ObservationId,
|
id: ObservationId,
|
||||||
addr: A,
|
addr: A,
|
||||||
|
kind: BounceKind,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if is_sourced(bounce, id, &addr) {
|
if is_sourced(bounce, id, &addr) {
|
||||||
return false;
|
return false;
|
||||||
@@ -638,10 +755,23 @@ fn record_bounce<A: PartialEq>(
|
|||||||
if bounce.is_full() {
|
if bounce.is_full() {
|
||||||
bounce.remove(0);
|
bounce.remove(0);
|
||||||
}
|
}
|
||||||
let _ = bounce.push((id, addr));
|
let _ = bounce.push((id, addr, kind));
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether `(id, addr)` has been recorded in `bcast_sent` — i.e. the local node
|
||||||
|
/// actually dispatched that broadcast to that peer. Used to validate inbound
|
||||||
|
/// `Ack` receipts against broadcasts we truly sent.
|
||||||
|
fn bcast_sent_contains<A: PartialEq>(
|
||||||
|
bcast_sent: &[(ObservationId, A)],
|
||||||
|
id: ObservationId,
|
||||||
|
addr: &A,
|
||||||
|
) -> bool {
|
||||||
|
bcast_sent
|
||||||
|
.iter()
|
||||||
|
.any(|(bid, baddr)| *bid == id && baddr == addr)
|
||||||
|
}
|
||||||
|
|
||||||
/// Encodes one frame of `obs` (whose id is `id`) into `out`.
|
/// Encodes one frame of `obs` (whose id is `id`) into `out`.
|
||||||
///
|
///
|
||||||
/// Starting a stream (`resume = None`), the whole observation goes out as one
|
/// Starting a stream (`resume = None`), the whole observation goes out as one
|
||||||
@@ -1873,4 +2003,310 @@ mod tests {
|
|||||||
"custody must not read broadcast observations"
|
"custody must not read broadcast observations"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drives `ex` so that `id` (addressed or broadcast) is emitted to `addr`:
|
||||||
|
/// contact, an empty summary (so the peer lacks everything), then poll
|
||||||
|
/// `next_frame` until an Obs carrying `id` goes out. The emit path records
|
||||||
|
/// the Sent bounce (addressed) or the `bcast_sent` entry (broadcast).
|
||||||
|
fn deliver_to(ex: &mut Exchange<u8>, store: &mut MemStore, addr: u8, id: ObservationId) {
|
||||||
|
ex.on_event(Event::Contact(addr), store, 0);
|
||||||
|
empty_summary(ex, store, addr, [2; 16]);
|
||||||
|
for _ in 0..consts::MAX_FRAME {
|
||||||
|
if matches!(
|
||||||
|
poll_frame(ex, store, consts::MAX_FRAME, 0),
|
||||||
|
Some((_, Frame::Obs { obs, .. }, _)) if obs.id() == id
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An Ack for an addressed observation the peer received releases custody:
|
||||||
|
/// the id is marked released and is no longer selected for forwarding.
|
||||||
|
#[test]
|
||||||
|
fn ack_releases_addressed_custody_after_sent() {
|
||||||
|
let peer: NodeId = [2; 16];
|
||||||
|
let mut ex = Exchange::new(ME);
|
||||||
|
let mut store = MemStore::new();
|
||||||
|
let x = addressed([3; 16], 1, peer, b"hi");
|
||||||
|
let x_id = x.id();
|
||||||
|
store.insert(x, 0).unwrap();
|
||||||
|
deliver_to(&mut ex, &mut store, 1, x_id);
|
||||||
|
assert!(
|
||||||
|
!store.is_released(&x_id),
|
||||||
|
"custody is held until the peer confirms delivery"
|
||||||
|
);
|
||||||
|
|
||||||
|
let ids = heapless::Vec::from_slice(&[x_id]).unwrap();
|
||||||
|
let ack = Frame::Ack { from: peer, ids };
|
||||||
|
ex.on_event(Event::Frame(1, frame_bytes(&ack)), &mut store, 0);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
store.is_released(&x_id),
|
||||||
|
"an Ack releases custody for a dispatched addressed id"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// After an Ack releases custody, a new contact drops the bounce records,
|
||||||
|
/// but the released id is still not selected: the is_released mark on the
|
||||||
|
/// stored entry keeps it out of forwarding.
|
||||||
|
#[test]
|
||||||
|
fn ack_released_id_will_reselect_and_reject() {
|
||||||
|
let peer: NodeId = [2; 16];
|
||||||
|
let mut ex = Exchange::new(ME);
|
||||||
|
let mut store = MemStore::new();
|
||||||
|
let x = addressed([3; 16], 1, peer, b"hi");
|
||||||
|
let x_id = x.id();
|
||||||
|
store.insert(x, 0).unwrap();
|
||||||
|
deliver_to(&mut ex, &mut store, 1, x_id);
|
||||||
|
|
||||||
|
let ids = heapless::Vec::from_slice(&[x_id]).unwrap();
|
||||||
|
let ack = Frame::Ack { from: peer, ids };
|
||||||
|
ex.on_event(Event::Frame(1, frame_bytes(&ack)), &mut store, 0);
|
||||||
|
assert!(store.is_released(&x_id), "an Ack releases custody of X");
|
||||||
|
|
||||||
|
// A new contact drops the bounce records; the released id must still be
|
||||||
|
// refused by the custody scan.
|
||||||
|
ex.on_event(Event::Lost(1), &mut store, 0);
|
||||||
|
ex.on_event(Event::Contact(1), &mut store, 0);
|
||||||
|
empty_summary(&mut ex, &mut store, 1, peer);
|
||||||
|
|
||||||
|
let mut reemitted = false;
|
||||||
|
for _ in 0..consts::MAX_FRAME {
|
||||||
|
if matches!(
|
||||||
|
poll_frame(&mut ex, &store, consts::MAX_FRAME, 0),
|
||||||
|
Some((_, Frame::Obs { obs, .. }, _)) if obs.id() == x_id
|
||||||
|
) {
|
||||||
|
reemitted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
!reemitted,
|
||||||
|
"a released id must not be selected again after a new contact"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An Ack for a broadcast we dispatched to the peer fires exactly one
|
||||||
|
/// ConfidenceEvent naming the peer and the broadcast id.
|
||||||
|
#[test]
|
||||||
|
fn ack_broadcast_fires_confidence_event() {
|
||||||
|
let peer: NodeId = [2; 16];
|
||||||
|
let mut ex = Exchange::new(ME);
|
||||||
|
let mut store = MemStore::new();
|
||||||
|
let b = obs([3; 16], 1, 0, None, b"t", b"bc");
|
||||||
|
let b_id = b.id();
|
||||||
|
store.insert(b, 0).unwrap();
|
||||||
|
deliver_to(&mut ex, &mut store, 1, b_id);
|
||||||
|
assert!(!store.is_released(&b_id));
|
||||||
|
|
||||||
|
let ids = heapless::Vec::from_slice(&[b_id]).unwrap();
|
||||||
|
let ack = Frame::Ack { from: peer, ids };
|
||||||
|
ex.on_event(Event::Frame(1, frame_bytes(&ack)), &mut store, 0);
|
||||||
|
|
||||||
|
let events = ex.drain_events();
|
||||||
|
assert_eq!(
|
||||||
|
events.len(),
|
||||||
|
1,
|
||||||
|
"one broadcast ack fires one confidence event"
|
||||||
|
);
|
||||||
|
assert_eq!(events[0].peer, peer);
|
||||||
|
assert_eq!(events[0].id, b_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A broadcast Ack does not release custody: there is no local addressed
|
||||||
|
/// copy to release, so the broadcast stays held.
|
||||||
|
#[test]
|
||||||
|
fn ack_broadcast_release_does_not_store_broadcast() {
|
||||||
|
let peer: NodeId = [2; 16];
|
||||||
|
let mut ex = Exchange::new(ME);
|
||||||
|
let mut store = MemStore::new();
|
||||||
|
let b = obs([3; 16], 1, 0, None, b"t", b"bc");
|
||||||
|
let b_id = b.id();
|
||||||
|
store.insert(b, 0).unwrap();
|
||||||
|
deliver_to(&mut ex, &mut store, 1, b_id);
|
||||||
|
assert!(
|
||||||
|
!store.is_released(&b_id),
|
||||||
|
"a broadcast is held, not released"
|
||||||
|
);
|
||||||
|
|
||||||
|
let ids = heapless::Vec::from_slice(&[b_id]).unwrap();
|
||||||
|
let ack = Frame::Ack { from: peer, ids };
|
||||||
|
ex.on_event(Event::Frame(1, frame_bytes(&ack)), &mut store, 0);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!store.is_released(&b_id),
|
||||||
|
"an Ack must not release a broadcast we dispatched"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An Ack for an id the local node only received (not dispatched) must NOT
|
||||||
|
/// release it: release applies to dispatched ids only.
|
||||||
|
#[test]
|
||||||
|
fn is_released_not_selected_after_ack() {
|
||||||
|
let peer: NodeId = [2; 16];
|
||||||
|
let mut ex = Exchange::new(ME);
|
||||||
|
let mut store = MemStore::new();
|
||||||
|
// A broadcast received from the peer: the node only received it.
|
||||||
|
let x = obs(peer, 1, 0, None, b"t", b"hi");
|
||||||
|
let x_id = x.id();
|
||||||
|
let frame = Frame::Obs { from: peer, obs: x };
|
||||||
|
ex.on_event(Event::Frame(1, frame_bytes(&frame)), &mut store, 0);
|
||||||
|
assert!(store.contains(&x_id));
|
||||||
|
assert!(store.get(&x_id, 0).unwrap().to.is_none());
|
||||||
|
|
||||||
|
let ids = heapless::Vec::from_slice(&[x_id]).unwrap();
|
||||||
|
let ack = Frame::Ack { from: peer, ids };
|
||||||
|
ex.on_event(Event::Frame(1, frame_bytes(&ack)), &mut store, 0);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!store.is_released(&x_id),
|
||||||
|
"an Ack must not release an id the local node only received"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The custody scan skips a released id but still offers non-released ids:
|
||||||
|
/// releasing the broadcast leaves the addressed id offered, and releasing
|
||||||
|
/// the addressed id leaves nothing offered.
|
||||||
|
#[test]
|
||||||
|
fn select_addressed_still_selects_non_released() {
|
||||||
|
let mut store = MemStore::new();
|
||||||
|
let a = addressed([3; 16], 1, [4; 16], b"a");
|
||||||
|
let b = obs([3; 16], 2, 0, None, b"t", b"bc");
|
||||||
|
let a_id = a.id();
|
||||||
|
let b_id = b.id();
|
||||||
|
store.insert(a, 0).unwrap();
|
||||||
|
store.insert(b, 0).unwrap();
|
||||||
|
|
||||||
|
// Release the broadcast only: the non-released addressed id is still
|
||||||
|
// offered.
|
||||||
|
store.release(&b_id);
|
||||||
|
let empty: &[(ObservationId, u8, BounceKind)] = &[];
|
||||||
|
let found = select_addressed(&store, 0, empty, &1u8, None, |_id, obs| obs.to.is_some());
|
||||||
|
assert_eq!(found.map(|(id, _)| id), Some(a_id));
|
||||||
|
|
||||||
|
// Release the addressed id too: nothing remains selectable.
|
||||||
|
store.release(&a_id);
|
||||||
|
let found = select_addressed(&store, 0, empty, &1u8, None, |_id, obs| obs.to.is_some());
|
||||||
|
assert!(found.is_none(), "an id released by an Ack is not selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// After an Ack releases custody, the release mark persists on the stored
|
||||||
|
/// entry across a new contact: dropping the exchange's bounce records does
|
||||||
|
/// not re-open the id, so it is never offered again.
|
||||||
|
#[test]
|
||||||
|
fn is_released_not_offered_after_contact_ends() {
|
||||||
|
let peer: NodeId = [2; 16];
|
||||||
|
let mut ex = Exchange::new(ME);
|
||||||
|
let mut store = MemStore::new();
|
||||||
|
let x = addressed([3; 16], 1, peer, b"hi");
|
||||||
|
let x_id = x.id();
|
||||||
|
store.insert(x, 0).unwrap();
|
||||||
|
deliver_to(&mut ex, &mut store, 1, x_id);
|
||||||
|
|
||||||
|
let ids = heapless::Vec::from_slice(&[x_id]).unwrap();
|
||||||
|
let ack = Frame::Ack { from: peer, ids };
|
||||||
|
ex.on_event(Event::Frame(1, frame_bytes(&ack)), &mut store, 0);
|
||||||
|
assert!(
|
||||||
|
store.is_released(&x_id),
|
||||||
|
"an Ack releases custody for a dispatched id"
|
||||||
|
);
|
||||||
|
|
||||||
|
// A new contact drops the exchange's bounce records, but the release
|
||||||
|
// mark on the stored entry persists.
|
||||||
|
ex.on_event(Event::Lost(1), &mut store, 0);
|
||||||
|
ex.on_event(Event::Contact(1), &mut store, 0);
|
||||||
|
assert!(
|
||||||
|
store.is_released(&x_id),
|
||||||
|
"the release mark persists across a new contact; the id is not re-offered"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A broadcast Ack from a peer that never held the broadcast fires no
|
||||||
|
/// ConfidenceEvent: the receipt names a broadcast we did not dispatch to
|
||||||
|
/// that peer.
|
||||||
|
#[test]
|
||||||
|
fn ack_broadcast_forged_id_no_confidence() {
|
||||||
|
let peer: NodeId = [2; 16];
|
||||||
|
let mut ex = Exchange::new(ME);
|
||||||
|
let mut store = MemStore::new();
|
||||||
|
let b = obs([3; 16], 1, 0, None, b"t", b"bc");
|
||||||
|
let b_id = b.id();
|
||||||
|
store.insert(b, 0).unwrap();
|
||||||
|
// Dispatch the broadcast to a different peer (addr 2), not addr 1.
|
||||||
|
deliver_to(&mut ex, &mut store, 2, b_id);
|
||||||
|
|
||||||
|
// Peer 1 (addr 1) claims to hold b_id though it never did.
|
||||||
|
let ids = heapless::Vec::from_slice(&[b_id]).unwrap();
|
||||||
|
let ack = Frame::Ack { from: peer, ids };
|
||||||
|
ex.on_event(Event::Frame(1, frame_bytes(&ack)), &mut store, 0);
|
||||||
|
|
||||||
|
let events = ex.drain_events();
|
||||||
|
assert!(
|
||||||
|
events.is_empty(),
|
||||||
|
"a broadcast Ack for a broadcast not sent to the peer fires no confidence event"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// drain_events returns confidence events oldest-first, in the order the
|
||||||
|
/// broadcasts were acknowledged.
|
||||||
|
#[test]
|
||||||
|
fn drain_events_returns_confidence_events_oldest_first() {
|
||||||
|
let peer: NodeId = [2; 16];
|
||||||
|
let mut ex = Exchange::new(ME);
|
||||||
|
let mut store = MemStore::new();
|
||||||
|
let a = obs([3; 16], 1, 0, None, b"t", b"bc-a");
|
||||||
|
let b = obs([3; 16], 2, 0, None, b"t", b"bc-b");
|
||||||
|
let a_id = a.id();
|
||||||
|
let b_id = b.id();
|
||||||
|
store.insert(a, 0).unwrap();
|
||||||
|
store.insert(b, 0).unwrap();
|
||||||
|
deliver_to(&mut ex, &mut store, 1, a_id);
|
||||||
|
deliver_to(&mut ex, &mut store, 1, b_id);
|
||||||
|
|
||||||
|
let ack_for = |ids: heapless::Vec<ObservationId, { consts::ACK_IDS_CAP }>| Frame::Ack {
|
||||||
|
from: peer,
|
||||||
|
ids,
|
||||||
|
};
|
||||||
|
|
||||||
|
let ids_a = heapless::Vec::from_slice(&[a_id]).unwrap();
|
||||||
|
ex.on_event(Event::Frame(1, frame_bytes(&ack_for(ids_a))), &mut store, 0);
|
||||||
|
let ids_b = heapless::Vec::from_slice(&[b_id]).unwrap();
|
||||||
|
ex.on_event(Event::Frame(1, frame_bytes(&ack_for(ids_b))), &mut store, 0);
|
||||||
|
|
||||||
|
let events = ex.drain_events();
|
||||||
|
assert_eq!(
|
||||||
|
events.len(),
|
||||||
|
2,
|
||||||
|
"each broadcast ack fires a confidence event"
|
||||||
|
);
|
||||||
|
assert_eq!(events[0].id, a_id, "events come back oldest-first");
|
||||||
|
assert_eq!(events[1].id, b_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// drain_events empties the queue: a second drain yields nothing.
|
||||||
|
#[test]
|
||||||
|
fn drain_events_empties_queue() {
|
||||||
|
let peer: NodeId = [2; 16];
|
||||||
|
let mut ex = Exchange::new(ME);
|
||||||
|
let mut store = MemStore::new();
|
||||||
|
let b = obs([3; 16], 1, 0, None, b"t", b"bc");
|
||||||
|
let b_id = b.id();
|
||||||
|
store.insert(b, 0).unwrap();
|
||||||
|
deliver_to(&mut ex, &mut store, 1, b_id);
|
||||||
|
|
||||||
|
let ids = heapless::Vec::from_slice(&[b_id]).unwrap();
|
||||||
|
let ack = Frame::Ack { from: peer, ids };
|
||||||
|
ex.on_event(Event::Frame(1, frame_bytes(&ack)), &mut store, 0);
|
||||||
|
|
||||||
|
let first = ex.drain_events();
|
||||||
|
assert_eq!(first.len(), 1, "the queued confidence event is drained");
|
||||||
|
assert_eq!(first[0].id, b_id);
|
||||||
|
|
||||||
|
let second = ex.drain_events();
|
||||||
|
assert!(
|
||||||
|
second.is_empty(),
|
||||||
|
"drain_events empties the queue; a second drain yields nothing"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub mod belief;
|
|||||||
pub mod bloom;
|
pub mod bloom;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
pub mod exchange;
|
pub mod exchange;
|
||||||
|
pub use exchange::ConfidenceEvent;
|
||||||
pub mod frag;
|
pub mod frag;
|
||||||
#[cfg(feature = "alloc")]
|
#[cfg(feature = "alloc")]
|
||||||
pub mod mem_store;
|
pub mod mem_store;
|
||||||
|
|||||||
Reference in New Issue
Block a user