use crate::{
Error,
peer::{CALL_TIMEOUT, Pending, SyncPeers, bind_response},
peer_limits::ESTABLISHED_PER_DIRECTION,
peer_protocol::{PARENT_LIMIT, Reply, Response},
source::{CommitSource, check_cancelled, verify_parent},
};
use bitcoin::{BlockHash, Txid};
use libp2p::PeerId;
use std::{
sync::atomic::AtomicBool,
time::{Duration, Instant},
};
use urma_chain::observation::Chain;
pub(crate) enum Parent {
Hex(String),
Absent,
Busy,
}
pub(crate) fn validate_parent(
genesis: BlockHash,
peer: PeerId,
pending: &Pending,
response: Response,
) -> Result<Parent, Error> {
bind_response(genesis, peer, pending, &response)?;
match response.result {
Reply::Parent(hex) if hex.len() <= 2 * PARENT_LIMIT => Ok(Parent::Hex(hex)),
Reply::Parent(_) => Err(Error::Invalid("peer parent exceeds byte limit".into())),
Reply::Busy => Ok(Parent::Busy),
Reply::Missing | Reply::TooLarge | Reply::WrongNetwork | Reply::UnsupportedVersion => {
Ok(Parent::Absent)
}
Reply::Record(_) | Reply::Inventory(_) | Reply::Peers(_) | Reply::Refused => Err(
Error::Invalid("peer answered a parent request with another kind or a refusal".into()),
),
}
}
pub struct OverlayCommitSource {
chain: Chain,
peers: SyncPeers,
}
impl OverlayCommitSource {
const PEERS_PER_FETCH: usize = ESTABLISHED_PER_DIRECTION;
const FETCH_DEADLINE: Duration = Duration::from_secs(20);
pub(crate) fn new(chain: Chain, peers: SyncPeers) -> Self {
Self { chain, peers }
}
}
impl CommitSource for OverlayCommitSource {
fn chain(&self) -> Chain {
self.chain
}
fn fetch(&self, txid: Txid, cancelled: &AtomicBool) -> Result<Option<Vec<u8>>, Error> {
check_cancelled(cancelled)?;
let deadline = Instant::now() + Self::FETCH_DEADLINE;
let mut answered = false;
let mut failure = None;
let window = self.peers.window(Self::PEERS_PER_FETCH);
let mut live_left = window.iter().filter(|peer| !peer.configured()).count();
for peer in &window {
check_cancelled(cancelled)?;
let left = deadline.saturating_duration_since(Instant::now());
let share = match peer.configured() {
true => Some(left.min(CALL_TIMEOUT)),
false => u32::try_from(live_left)
.ok()
.and_then(|peers| left.checked_div(peers)),
};
let Some(share) = share.filter(|share| !share.is_zero()) else {
break;
};
live_left = live_left.saturating_sub(usize::from(!peer.configured()));
match peer.parent(txid, Instant::now() + share) {
Ok(Parent::Hex(hex)) => match self.decode(&hex, txid) {
Ok(raw) => return Ok(Some(raw)),
Err(error) => {
peer.disconnect();
failure = Some(error);
}
},
Ok(Parent::Absent) => answered = true,
Ok(Parent::Busy) | Err(Error::Busy(_)) => (),
Err(Error::DialDenied(_local_capacity)) => (),
Err(error) => {
peer.disconnect();
failure = Some(error);
}
}
}
match failure {
Some(error) if !answered => Err(Error::CommitSource(format!("overlay: {error}"))),
_ => Ok(None),
}
}
}
impl OverlayCommitSource {
fn decode(&self, hex: &str, txid: Txid) -> Result<Vec<u8>, Error> {
if !hex
.bytes()
.all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
{
return Err(Error::Invalid("peer parent must be lowercase hex".into()));
}
let raw = hex::decode(hex)
.map_err(|error| Error::Invalid(format!("peer parent hex: {error}")))?;
verify_parent(self.chain, txid, &raw)?;
Ok(raw)
}
}