use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use chia_protocol::{Bytes32, CoinState};
use tokio::sync::{mpsc, Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SessionId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FrameSource {
pub address: SocketAddr,
pub session: SessionId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionEndReason {
Disconnected,
UndecodableFrame,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PoolFrame {
Reset,
Peak { height: u32, header_hash: Bytes32 },
CoinStates {
height: u32,
fork_height: u32,
items: Vec<CoinState>,
},
SessionEnded { reason: SessionEndReason },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourcedFrame {
pub source: FrameSource,
pub frame: PoolFrame,
}
pub struct FrameSubscription {
receiver: mpsc::Receiver<SourcedFrame>,
}
impl FrameSubscription {
pub async fn recv(&mut self) -> Option<SourcedFrame> {
self.receiver.recv().await
}
pub fn try_recv(&mut self) -> Result<SourcedFrame, mpsc::error::TryRecvError> {
self.receiver.try_recv()
}
}
pub struct FrameFanout {
subscribers: Mutex<Vec<mpsc::Sender<SourcedFrame>>>,
next_session: AtomicU64,
}
impl Default for FrameFanout {
fn default() -> Self {
Self::new()
}
}
impl FrameFanout {
pub fn new() -> Self {
Self {
subscribers: Mutex::new(Vec::new()),
next_session: AtomicU64::new(0),
}
}
pub async fn subscribe(self: &Arc<Self>, capacity: usize) -> FrameSubscription {
let (sender, receiver) = mpsc::channel(capacity.max(1));
self.subscribers.lock().await.push(sender);
FrameSubscription { receiver }
}
pub async fn subscriber_count(&self) -> usize {
self.subscribers.lock().await.len()
}
pub fn allocate_session(&self, address: SocketAddr) -> FrameSource {
FrameSource {
address,
session: SessionId(self.next_session.fetch_add(1, Ordering::Relaxed)),
}
}
pub async fn open_session(&self, source: FrameSource) {
self.publish(source, PoolFrame::Reset).await;
}
pub async fn publish(&self, source: FrameSource, frame: PoolFrame) {
let sourced = SourcedFrame { source, frame };
let mut subscribers = self.subscribers.lock().await;
subscribers.retain(|sender| match sender.try_send(sourced.clone()) {
Ok(()) => true,
Err(mpsc::error::TrySendError::Full(_)) => {
log::warn!(
"frame subscriber fell behind; terminating its subscription rather than \
dropping a frame it would never learn it missed"
);
false
}
Err(mpsc::error::TrySendError::Closed(_)) => false,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr};
fn addr(last: u8) -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, last)), 8444)
}
fn source(fanout: &FrameFanout, last: u8) -> FrameSource {
fanout.allocate_session(addr(last))
}
fn peak(height: u32) -> PoolFrame {
PoolFrame::Peak {
height,
header_hash: Bytes32::new([height as u8; 32]),
}
}
#[tokio::test]
async fn a_frame_names_the_peer_that_sent_it() {
let fanout = Arc::new(FrameFanout::new());
let mut subscription = fanout.subscribe(8).await;
let honest = source(&fanout, 1);
let liar = source(&fanout, 2);
fanout.publish(honest, peak(100)).await;
fanout.publish(liar, peak(999)).await;
let first = subscription.try_recv().expect("the honest frame");
let second = subscription.try_recv().expect("the injected frame");
assert_eq!(first.source.address, addr(1));
assert_eq!(second.source.address, addr(2));
assert_ne!(
first.source, second.source,
"two peers must not share one frame identity, or an injected frame is \
indistinguishable from the followed peer's"
);
assert_eq!(
second.frame,
peak(999),
"the injected frame is still delivered — attribution is what lets a subscriber \
reject and eject its sender, not a filter here"
);
}
#[tokio::test]
async fn a_second_session_does_not_change_the_identity_of_the_first() {
let fanout = Arc::new(FrameFanout::new());
let mut subscription = fanout.subscribe(8).await;
let first = source(&fanout, 1);
fanout.publish(first, peak(100)).await;
let second = source(&fanout, 2);
fanout.open_session(second).await;
fanout.publish(second, peak(101)).await;
fanout.publish(first, peak(102)).await;
let mut from_first = Vec::new();
while let Ok(sourced) = subscription.try_recv() {
if sourced.source == first {
from_first.push(sourced.frame);
}
}
assert_eq!(
from_first,
vec![peak(100), peak(102)],
"both of peer 1's frames must arrive under peer 1's own identity, before and after \
peer 2 connected"
);
assert_ne!(first.session, second.session, "sessions must be distinct");
}
#[tokio::test]
async fn opening_a_session_resets_only_that_session() {
let fanout = Arc::new(FrameFanout::new());
let mut subscription = fanout.subscribe(8).await;
let followed = source(&fanout, 1);
fanout.open_session(followed).await;
fanout.publish(followed, peak(100)).await;
let other = source(&fanout, 2);
fanout.open_session(other).await;
let mut resets_for_followed = 0usize;
let mut resets_for_other = 0usize;
while let Ok(sourced) = subscription.try_recv() {
if sourced.frame == PoolFrame::Reset {
if sourced.source == followed {
resets_for_followed += 1;
} else if sourced.source == other {
resets_for_other += 1;
}
}
}
assert_eq!(
resets_for_followed, 1,
"the followed session must be reset exactly once — when it opened, and never because \
another peer connected"
);
assert_eq!(resets_for_other, 1);
}
#[tokio::test]
async fn reset_is_delivered_before_the_first_frame_of_its_session() {
let fanout = Arc::new(FrameFanout::new());
let mut subscription = fanout.subscribe(8).await;
let session = source(&fanout, 1);
fanout.open_session(session).await;
fanout.publish(session, peak(101)).await;
let mut seen = Vec::new();
while let Ok(sourced) = subscription.try_recv() {
seen.push(sourced.frame);
}
let reset_at = seen
.iter()
.position(|f| *f == PoolFrame::Reset)
.expect("a session must announce itself with a Reset");
let first_frame_at = seen
.iter()
.position(|f| matches!(f, PoolFrame::Peak { height: 101, .. }))
.expect("the session's frame must be delivered");
assert!(
reset_at < first_frame_at,
"Reset must precede the first frame of its session: {seen:?}"
);
}
#[tokio::test]
async fn a_subscriber_that_overflows_is_terminated_rather_than_missing_a_frame() {
let fanout = Arc::new(FrameFanout::new());
let mut subscription = fanout.subscribe(2).await;
let session = source(&fanout, 1);
for height in 1..=4u32 {
fanout.publish(session, peak(height)).await;
}
let mut delivered = Vec::new();
let ended = loop {
match subscription.try_recv() {
Ok(SourcedFrame {
frame: PoolFrame::Peak { height, .. },
..
}) => delivered.push(height),
Ok(_) => {}
Err(err) => break err,
}
};
assert_eq!(
delivered,
vec![1, 2],
"only the frames that fit may be delivered, and the stream must then end"
);
assert_eq!(
ended,
mpsc::error::TryRecvError::Disconnected,
"the subscription must be TERMINATED, not merely empty with frames silently skipped"
);
assert_eq!(
fanout.subscriber_count().await,
0,
"the overflowing subscription must be dropped, not retained and thinned"
);
}
#[tokio::test]
async fn a_subscriber_that_keeps_up_stays_subscribed() {
let fanout = Arc::new(FrameFanout::new());
let mut subscription = fanout.subscribe(2).await;
let session = source(&fanout, 1);
for height in 1..=4u32 {
fanout.publish(session, peak(height)).await;
assert!(matches!(
subscription.try_recv(),
Ok(SourcedFrame {
frame: PoolFrame::Peak { .. },
..
})
));
}
assert_eq!(fanout.subscriber_count().await, 1);
}
}