use crate::{
Error,
engine::Engine,
model::{RecordPackage, RecordReceipt},
peer::{Answer, CALL_TIMEOUT, PeerSource, Pending, bind_response},
peer_protocol::{
INVENTORY_ENTRIES, INVENTORY_LIMIT, INVENTORY_SPAN, InventoryPage, Kind, PARENT_LIMIT,
Placement, Reply, Request, Response,
},
source::check_cancelled,
};
use bitcoin::{BlockHash, Txid};
use libp2p::PeerId;
use std::{sync::atomic::AtomicBool, time::Instant};
pub(crate) fn lookup(engine: &Engine, request: &Request) -> Reply {
match request.kind {
Kind::Record { txid } => match engine.record(txid) {
Ok(Some(record)) => Reply::Record(Box::new(record.package)),
Ok(None) => Reply::Missing,
Err(_) => Reply::Busy,
},
Kind::Parent { txid } => match engine.raw_transaction(txid) {
Ok(Some(raw)) if raw.len() > PARENT_LIMIT => Reply::TooLarge,
Ok(Some(raw)) => Reply::Parent(hex::encode(raw)),
Ok(None) => Reply::Missing,
Err(_) => Reply::Busy,
},
Kind::Inventory {
from_height,
to_height,
after,
limit,
} => inventory(engine, (from_height, to_height), after, limit),
Kind::Peers { .. } => Reply::Refused,
}
}
fn admissible(from: u64, to: u64, limit: u32) -> bool {
to >= from && to - from <= INVENTORY_SPAN && (1..=INVENTORY_ENTRIES).contains(&limit)
}
fn inventory(engine: &Engine, range: (u64, u64), after: Option<Placement>, limit: u32) -> Reply {
const ENVELOPE: usize = 1024;
let (from, to) = range;
if !admissible(from, to, limit) {
return Reply::Refused;
}
let cursor = after.map(|placement| (placement.height, placement.position));
let rows = match engine.inventory_page(from, to, cursor, limit) {
Ok(rows) => rows,
Err(_) => return Reply::Busy,
};
let entries = rows
.entries
.into_iter()
.map(|(block, position, txid, wtxid)| Placement {
height: block.height,
block_hash: block.hash,
position,
txid,
wtxid,
})
.collect();
let page = InventoryPage {
scanned_from: rows.scanned_from,
scanned_through: rows.scanned_through,
entries,
more: rows.more,
};
match serde_json::to_vec(&page) {
Ok(bytes) if bytes.len() + ENVELOPE <= INVENTORY_LIMIT => Reply::Inventory(page),
_ => Reply::TooLarge,
}
}
pub(crate) fn validate_inventory(
genesis: BlockHash,
peer: PeerId,
pending: &Pending,
response: Response,
) -> Result<Option<InventoryPage>, Error> {
bind_response(genesis, peer, pending, &response)?;
match response.result {
Reply::Inventory(page) => check_page(&response.kind, page).map(Some),
Reply::Missing => Ok(None),
Reply::Busy => Err(Error::PeerBusy),
Reply::Record(_) | Reply::Parent(_) | Reply::Peers(_) => Err(Error::Invalid(
"peer answered an inventory request with another kind".into(),
)),
Reply::Refused | Reply::TooLarge | Reply::WrongNetwork | Reply::UnsupportedVersion => {
Err(Error::Invalid("peer refused the inventory request".into()))
}
}
}
fn check_page(kind: &Kind, page: InventoryPage) -> Result<InventoryPage, Error> {
let Kind::Inventory {
from_height,
to_height,
after,
limit,
} = *kind
else {
return Err(Error::Invalid("inventory reply to another request".into()));
};
let limit = usize::try_from(limit).map_err(|e| Error::Invalid(e.to_string()))?;
if page.entries.len() > limit {
return Err(Error::Invalid(
"peer inventory exceeds the requested limit".into(),
));
}
let mut previous = after.map(|placement| (placement.height, placement.position));
for entry in &page.entries {
let at = (entry.height, entry.position);
let ordered = previous.is_none_or(|last| at > last);
if entry.height < from_height || entry.height > to_height || !ordered {
return Err(Error::Invalid(
"peer inventory entry out of range or order".into(),
));
}
previous = Some(at);
}
Ok(page)
}
pub(crate) fn selected(engine: &Engine, entry: &Placement) -> Result<bool, Error> {
match engine.archive()?.ticket(entry.height) {
Ok(ticket) => Ok(ticket.point().hash == entry.block_hash),
Err(Error::Invalid(_outside_selected_view)) => Ok(false),
Err(error) => Err(error),
}
}
pub(crate) fn placed(receipt: &RecordReceipt, entry: &Placement) -> bool {
receipt.block.height == entry.height
&& receipt.block.hash == entry.block_hash
&& receipt.position == entry.position
&& receipt.txid == entry.txid
&& receipt.wtxid == entry.wtxid
}
impl PeerSource {
pub fn inventory(
&self,
from_height: u64,
to_height: u64,
after: Option<Placement>,
limit: u32,
cancelled: &AtomicBool,
) -> Result<Option<InventoryPage>, Error> {
let range = (from_height, to_height);
let deadline = Instant::now() + CALL_TIMEOUT;
match self.inventory_until(range, after, limit, cancelled, deadline) {
Err(Error::Busy(_) | Error::PeerBusy) => Ok(None),
other => other,
}
}
pub(crate) fn fetch_until(
&self,
txid: Txid,
deadline: Instant,
) -> Result<Option<RecordPackage>, Error> {
self.request(Kind::Record { txid }, deadline, Answer::Record)
}
pub(crate) fn inventory_until(
&self,
(from_height, to_height): (u64, u64),
after: Option<Placement>,
limit: u32,
cancelled: &AtomicBool,
deadline: Instant,
) -> Result<Option<InventoryPage>, Error> {
check_cancelled(cancelled)?;
let kind = Kind::Inventory {
from_height,
to_height,
after,
limit,
};
self.request(kind, deadline, Answer::Inventory)
}
}