use super::Scheduler;
use crate::{
Error,
engine::Engine,
peer_protocol::{InventoryPage, Placement},
};
pub(super) struct Range {
pub(super) from: u64,
pub(super) to: u64,
pub(super) after: Option<Placement>,
pub(super) entries: Vec<Placement>,
pub(super) next: usize,
pub(super) more: bool,
pub(super) scanned_through: u64,
pub(super) generation: u64,
pub(super) missing: u32,
}
pub(super) enum Accepted {
Walk,
Unscanned,
Floor(u64),
}
pub(super) enum Opened {
Range(Range),
Synced,
AboveTip,
}
impl Opened {
pub(super) fn range(self) -> Option<Range> {
match self {
Opened::Range(range) => Some(range),
Opened::Synced | Opened::AboveTip => None,
}
}
}
pub(super) fn open(id: &str, engine: &Engine, floor: u64) -> Result<Opened, Error> {
let status = engine.status()?;
let Some(tip) = status.selected_tip else {
return Ok(Opened::Synced);
};
if floor > tip.height {
return Ok(Opened::AboveTip);
}
let from = engine.peer_cursor_from(id, floor)?;
if from > tip.height {
return Ok(Opened::Synced);
}
let span_end = from
.checked_add(Scheduler::RANGE_HEIGHTS - 1)
.ok_or_else(|| Error::Invalid("peer cursor overflows the height range".into()))?;
Ok(Opened::Range(Range {
from,
to: tip.height.min(span_end),
after: None,
entries: Vec::new(),
next: 0,
more: true,
scanned_through: 0,
generation: status.generation,
missing: 0,
}))
}
pub(super) fn accept(range: &mut Range, page: InventoryPage) -> Result<Accepted, Error> {
if page.scanned_from > range.from {
return Ok(Accepted::Floor(page.scanned_from));
}
let Some(scanned_through) = page.scanned_through.filter(|height| *height >= range.from) else {
return Ok(Accepted::Unscanned);
};
let limit = usize::try_from(Scheduler::PAGE_ENTRIES)
.map_err(|error| Error::Invalid(format!("page limit: {error}")))?;
if page.entries.len() > limit || (page.more && page.entries.is_empty()) {
return Err(Error::Invalid("peer inventory page size is invalid".into()));
}
let mut previous = range.after;
for entry in &page.entries {
if entry.height < range.from
|| entry.height > range.to
|| previous
.is_some_and(|last| (entry.height, entry.position) <= (last.height, last.position))
{
return Err(Error::Invalid(
"peer inventory entry is out of range or order".into(),
));
}
previous = Some(*entry);
}
range.after = previous;
range.entries = page
.entries
.into_iter()
.filter(|entry| entry.height <= scanned_through)
.collect();
range.next = 0;
range.more = page.more;
range.scanned_through = scanned_through;
Ok(Accepted::Walk)
}