use std::collections::VecDeque;
use std::time::Duration;
use tokio::time::Instant;
use crate::fleet::view::LogCoverage;
use crate::fleet::FleetView;
use crate::models::ResolvedTopology;
use crate::ship::wire::{encode_status, StatusBody, STATUS_V0_LEN};
use crate::ship::{BlockPos, Range};
use super::client::{mono, ClientSide};
pub(super) const MAX_QUEUED_STATUS: usize = 1024;
const STATUS_V0_NO_CHAIN_ID: usize = STATUS_V0_LEN - 32;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum StatusStep {
Reply(bool),
Relay(bool),
}
#[derive(Debug, Default)]
pub(super) struct StatusFlow {
hold: VecDeque<bool>,
backlog: VecDeque<bool>,
deadline: Option<Instant>,
}
impl StatusFlow {
pub fn push(&mut self, v1: bool, now: Instant, wait: Duration) -> bool {
if self.hold.len() + self.backlog.len() >= MAX_QUEUED_STATUS {
return false;
}
self.hold.push_back(v1);
self.deadline.get_or_insert(now + wait);
true
}
pub fn next(&mut self, ready: bool, can_relay: bool) -> Option<StatusStep> {
if self.hold.is_empty() {
self.deadline = None;
return None;
}
let step = if !self.backlog.is_empty() || !ready {
if !can_relay {
return None;
}
let v1 = self.hold.pop_front()?;
self.backlog.push_back(v1);
StatusStep::Relay(v1)
} else {
StatusStep::Reply(self.hold.pop_front()?)
};
if self.hold.is_empty() {
self.deadline = None;
}
Some(step)
}
pub fn reply_arrived(&mut self) -> Option<bool> {
self.backlog.pop_front()
}
pub fn link_ended(&mut self, now: Instant, wait: Duration) {
while let Some(v1) = self.backlog.pop_back() {
self.hold.push_front(v1);
}
if !self.hold.is_empty() {
self.deadline.get_or_insert(now + wait);
}
}
pub fn waiting(&self) -> bool {
!self.hold.is_empty()
}
pub fn deadline(&self) -> Option<Instant> {
self.deadline
}
}
pub(super) fn status_ready(view: &FleetView, tip: (Option<BlockPos>, Option<BlockPos>)) -> bool {
tip.0.is_some()
&& tip.1.is_some()
&& (view.topology == ResolvedTopology::Pool || view.frozen_unknown == 0)
}
pub(super) fn status_range(cov: &LogCoverage, head: u32) -> Range {
let mut range = cov.top;
if !range.is_empty() && cov.live_tail_begin.is_some_and(|b| b <= range.end) {
range.end = range.end.max(head.saturating_add(1));
}
range
}
pub(super) struct RouterStatus {
pub bytes: Vec<u8>,
pub zero_chain_id: bool,
}
pub(super) fn router_status(
view: &FleetView,
tip: (Option<BlockPos>, Option<BlockPos>),
c: &mut ClientSide,
v1: bool,
) -> Option<RouterStatus> {
let (Some(head), Some(lib)) = tip else {
return None;
};
let head = mono(head, c.head_sent);
let mut lib = mono(lib, c.lib_sent);
if lib.num > head.num {
lib = head;
}
c.head_sent = Some(head);
c.lib_sent = Some(lib);
let body = StatusBody {
head,
lib,
trace: status_range(&view.trace, head.num),
chain_state: status_range(&view.chain_state, head.num),
finality: status_range(&view.finality, head.num),
chain_id: view.chain_id.unwrap_or([0; 32]),
};
let mut bytes = encode_status(v1, &body);
let missing = view.chain_id.is_none();
if missing && !v1 {
bytes.truncate(STATUS_V0_NO_CHAIN_ID);
}
Some(RouterStatus {
bytes,
zero_chain_id: missing && v1,
})
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::fleet::view::fixtures::{live, pos, states, view};
use crate::fleet::ObservedTip;
use crate::models::ServerState;
use crate::ship::wire::STATUS_V1_LEN;
const WAIT: Duration = Duration::from_secs(10);
#[test]
fn requests_are_answered_relayed_or_held_in_order() {
let now = Instant::now();
let mut flow = StatusFlow::default();
assert!(flow.push(false, now, WAIT));
assert_eq!(flow.next(true, true), Some(StatusStep::Reply(false)));
assert_eq!(flow.next(true, true), None);
assert_eq!(flow.deadline(), None);
assert!(flow.push(true, now, WAIT));
assert_eq!(flow.next(false, true), Some(StatusStep::Relay(true)));
assert!(flow.push(false, now, WAIT));
assert_eq!(flow.next(true, true), Some(StatusStep::Relay(false)));
assert_eq!(flow.reply_arrived(), Some(true));
assert_eq!(flow.reply_arrived(), Some(false));
assert_eq!(flow.reply_arrived(), None, "unsolicited");
assert!(flow.push(true, now, WAIT));
assert_eq!(flow.next(false, false), None);
assert!(flow.waiting());
assert_eq!(flow.deadline(), Some(now + WAIT));
assert_eq!(flow.next(true, false), Some(StatusStep::Reply(true)));
assert!(!flow.waiting());
assert_eq!(flow.deadline(), None);
}
#[test]
fn relayed_requests_survive_a_failover_in_order() {
let now = Instant::now();
let mut flow = StatusFlow::default();
for v1 in [true, false, true] {
assert!(flow.push(v1, now, WAIT));
assert!(matches!(flow.next(false, true), Some(StatusStep::Relay(_))));
}
assert_eq!(flow.reply_arrived(), Some(true));
assert!(flow.push(false, now, WAIT));
flow.link_ended(now, WAIT);
assert_eq!(flow.deadline(), Some(now + WAIT));
let mut order = Vec::new();
while let Some(step) = flow.next(true, false) {
order.push(step);
}
assert_eq!(
order,
vec![
StatusStep::Reply(false),
StatusStep::Reply(true),
StatusStep::Reply(false)
]
);
}
#[test]
fn the_queue_is_bounded() {
let now = Instant::now();
let mut flow = StatusFlow::default();
for _ in 0..MAX_QUEUED_STATUS {
assert!(flow.push(false, now, WAIT));
}
assert!(!flow.push(false, now, WAIT));
assert!(matches!(
flow.next(false, true),
Some(StatusStep::Relay(false))
));
assert!(!flow.push(false, now, WAIT), "relayed requests count too");
assert_eq!(flow.reply_arrived(), Some(false));
assert!(flow.push(false, now, WAIT));
}
fn pool_view(list: Vec<ServerState>, chain_id: Option<[u8; 32]>) -> FleetView {
let ctx = crate::fleet::view::BuildCtx {
pinned_chain_id: chain_id,
..crate::fleet::view::fixtures::ctx(ResolvedTopology::Pool)
};
let mut mem = crate::fleet::view::HubMemory::new(std::time::Instant::now());
let tips: Vec<Arc<ObservedTip>> = (0..list.len()).map(|_| Arc::default()).collect();
crate::fleet::view::build_view(
&ctx,
&states(list),
&tips,
&mut mem,
std::time::Instant::now(),
)
}
#[test]
fn pool_status_aggregates_the_fleet() {
let mut a = live(0, 2, 1000);
a.chain_state_begin_block = 5;
let b = live(1, 900, 1200);
let v = pool_view(vec![a, b], Some([7; 32]));
let tip = (v.head, v.lib);
assert_eq!(tip.0, Some(pos(1200)));
assert!(status_ready(&v, tip));
let mut c = ClientSide::default();
let reply = router_status(&v, tip, &mut c, false).unwrap();
let s = mock_ship::client::StatusView::parse(&reply.bytes).unwrap();
assert_eq!(s.variant, 0);
assert_eq!(s.len, STATUS_V0_LEN);
assert_eq!(s.head.0, 1200);
assert_eq!(s.lib.0, 1190);
assert_eq!(s.trace, (2, 1201));
assert_eq!(s.chain_state, (5, 1201));
assert_eq!(s.chain_id, Some([7; 32]));
assert!(!reply.zero_chain_id);
let fresher = (Some(pos(1300)), Some(pos(1290)));
let s = router_status(&v, fresher, &mut c, true).unwrap();
let s = mock_ship::client::StatusView::parse(&s.bytes).unwrap();
assert_eq!((s.variant, s.len), (3, STATUS_V1_LEN));
assert_eq!(s.trace, (2, 1301));
assert_eq!(s.finality, Some((0, 0)));
let older = (Some(pos(1100)), Some(pos(1050)));
let s = router_status(&v, older, &mut c, false).unwrap();
let s = mock_ship::client::StatusView::parse(&s.bytes).unwrap();
assert_eq!((s.head.0, s.lib.0), (1300, 1290));
assert_eq!(c.head_sent, Some(pos(1300)));
assert!(!status_ready(&v, (None, None)));
assert!(router_status(&v, (None, None), &mut c, false).is_none());
}
#[test]
fn without_a_chain_id_v0_omits_it_and_v1_sends_zeros() {
let v = pool_view(vec![live(0, 1, 500)], None);
assert_eq!(v.chain_id, None);
let tip = (v.head, v.lib);
let mut c = ClientSide::default();
let v0 = router_status(&v, tip, &mut c, false).unwrap();
assert_eq!(v0.bytes.len(), STATUS_V0_NO_CHAIN_ID);
assert!(!v0.zero_chain_id);
let parsed = mock_ship::client::StatusView::parse(&v0.bytes).unwrap();
assert_eq!(parsed.chain_id, None);
let v1 = router_status(&v, tip, &mut c, true).unwrap();
assert_eq!(v1.bytes.len(), STATUS_V1_LEN);
assert!(v1.zero_chain_id);
assert_eq!(
mock_ship::client::StatusView::parse(&v1.bytes)
.unwrap()
.chain_id,
Some([0; 32])
);
}
#[test]
fn stitched_status_waits_for_unreported_frozen_upstreams() {
use crate::fleet::view::fixtures::frozen_unreported;
let v = view(vec![live(0, 900, 1200), frozen_unreported(1, None)]);
assert!(!status_ready(&v, (v.head, v.lib)));
let v = view(vec![
live(0, 900, 1200),
frozen_unreported(1, Some((2, 900))),
]);
assert!(status_ready(&v, (v.head, v.lib)));
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn position(p: BlockPos) -> serde_json::Value {
serde_json::json!({ "block_num": p.num, "block_id": hex(&p.id) })
}
#[test]
fn status_replies_match_the_ship_abi_serializer() {
let abieos = rs_abieos::Abieos::new();
abieos
.set_abi_json("ship", mock_ship::SHIP_ABI_JSON)
.expect("SHiP ABI loads");
let body = StatusBody {
head: pos(2300),
lib: pos(2290),
trace: Range {
begin: 2,
end: 2301,
},
chain_state: Range {
begin: 5,
end: 2301,
},
finality: Range {
begin: 950,
end: 2301,
},
chain_id: [0xAB; 32],
};
let common = |with_chain_id: bool| {
let mut fields = serde_json::json!({
"head": position(body.head),
"last_irreversible": position(body.lib),
"trace_begin_block": body.trace.begin,
"trace_end_block": body.trace.end,
"chain_state_begin_block": body.chain_state.begin,
"chain_state_end_block": body.chain_state.end,
});
if with_chain_id {
fields["chain_id"] = serde_json::json!(hex(&body.chain_id));
}
fields
};
let to_bin = |json: serde_json::Value| {
abieos
.json_to_bin("ship", "result", &json.to_string())
.expect("status serializes")
};
let v0 = to_bin(serde_json::json!(["get_status_result_v0", common(true)]));
assert_eq!(encode_status(false, &body), v0);
assert_eq!(v0.len(), STATUS_V0_LEN);
let mut v1_fields = common(true);
v1_fields["finality_data_begin_block"] = serde_json::json!(body.finality.begin);
v1_fields["finality_data_end_block"] = serde_json::json!(body.finality.end);
let v1 = to_bin(serde_json::json!(["get_status_result_v1", v1_fields]));
assert_eq!(encode_status(true, &body), v1);
assert_eq!(v1.len(), STATUS_V1_LEN);
let bare = to_bin(serde_json::json!(["get_status_result_v0", common(false)]));
assert_eq!(bare.len(), STATUS_V0_NO_CHAIN_ID);
let mut ours = encode_status(false, &body);
ours.truncate(STATUS_V0_NO_CHAIN_ID);
assert_eq!(ours, bare);
let parsed = crate::zcd::deserialize_status_result(&ours[1..]).expect("decodes");
assert!(matches!(
parsed.get("head_block_num"),
Ok(Some(crate::zcd::ZCDValues::U32(2300)))
));
}
}