use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tape_api::program::tapedrive;
use tape_crypto::Address;
use tape_crypto::hash::Hash;
use tape_protocol::api::ApiError;
use tracing::debug;
use super::{BootstrapState, BootstrapStore, NetworkKey, PeerHealth, PeerRecord, Prediction};
pub const HEDGE_DELAY: Duration = Duration::from_millis(150);
const UNCONFIRMED_GENESIS: Hash = Hash([0u8; 32]);
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x100_0000_01b3;
pub fn counts_against_peer(error: &ApiError) -> bool {
match error {
ApiError::NotFound
| ApiError::NotResponsible
| ApiError::BlacklistedObject
| ApiError::NotInCommittee
| ApiError::RateLimited { .. } => false,
ApiError::NodeUnresolved(_)
| ApiError::ConnectionFailed(_)
| ApiError::Timeout
| ApiError::Serialization(_)
| ApiError::ServerError { .. }
| ApiError::StaleTrackProof
| ApiError::Other(_) => true,
}
}
pub fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Rank {
Measured,
Unmeasured,
Suspect,
Quarantined,
}
fn rank_of(record: Option<&PeerRecord>, now: u64) -> (Rank, u32) {
match record {
None => (Rank::Unmeasured, 0),
Some(record) => match record.health {
PeerHealth::Quarantined { until } if now < until => (Rank::Quarantined, 0),
PeerHealth::Quarantined { .. } | PeerHealth::Suspect => (Rank::Suspect, 0),
PeerHealth::Healthy if record.ewma_micros == 0 => (Rank::Unmeasured, 0),
PeerHealth::Healthy => (Rank::Measured, record.ewma_micros),
},
}
}
pub struct Reputation {
state: Mutex<BootstrapState>,
store: BootstrapStore,
dirty: AtomicBool,
salt: u64,
}
impl Reputation {
pub fn ephemeral(network: NetworkKey) -> Self {
Self::from_state(BootstrapStore::disabled(), BootstrapState::new(network))
}
pub fn detached() -> Self {
Self::attach(BootstrapStore::disabled(), tapedrive::id().into())
}
pub fn attach(store: BootstrapStore, program_id: Address) -> Self {
let placeholder = NetworkKey {
program_id,
genesis: UNCONFIRMED_GENESIS,
};
let state = store
.load_any()
.filter(|state| state.network.program_id == program_id)
.unwrap_or_else(|| BootstrapState::new(placeholder));
Self::from_state(store, state)
}
fn from_state(store: BootstrapStore, mut state: BootstrapState) -> Self {
let now = now_secs();
for record in &mut state.peers {
record.reconcile_clock(now);
record.expire_quarantine(now);
}
Self {
state: Mutex::new(state),
store,
dirty: AtomicBool::new(false),
salt: u64::from(std::process::id()),
}
}
pub fn rekey(&self, network: NetworkKey) {
let Ok(mut state) = self.state.lock() else {
return;
};
if state.network == network {
return;
}
let is_unconfirmed = state.network.genesis == UNCONFIRMED_GENESIS;
let is_same_chain = is_unconfirmed || state.network.genesis == network.genesis;
match is_same_chain {
true => state.network = network,
false => *state = BootstrapState::new(network),
}
self.dirty.store(true, Ordering::Relaxed);
}
pub fn network(&self) -> Option<NetworkKey> {
self.state.lock().ok().map(|state| state.network)
}
pub fn record_success(&self, node: Address, latency: Duration) {
let now = now_secs();
let millis = latency.as_secs_f64() * 1_000.0;
if let Ok(mut state) = self.state.lock() {
state.peer_mut(node).record_success(now, millis);
}
self.dirty.store(true, Ordering::Relaxed);
}
pub fn record_failure(&self, node: Address) {
let now = now_secs();
let is_quarantined = match self.state.lock() {
Ok(mut state) => {
let record = state.peer_mut(node);
record.record_failure(now);
matches!(record.health, PeerHealth::Quarantined { .. })
}
Err(_) => false,
};
if is_quarantined {
debug!(%node, "peer quarantined after repeated failures");
}
self.dirty.store(true, Ordering::Relaxed);
}
pub fn is_quarantined(&self, node: &Address) -> bool {
let now = now_secs();
self.state
.lock()
.ok()
.and_then(|state| state.peer(node).map(|record| !record.is_available(now)))
.unwrap_or(false)
}
pub fn order(&self, peers: &[Address]) -> Vec<Address> {
let now = now_secs();
let Ok(state) = self.state.lock() else {
return peers.to_vec();
};
let mut ordered = peers.to_vec();
ordered.sort_by_cached_key(|node| {
let (rank, ewma) = rank_of(state.peer(node), now);
(rank, ewma, self.tiebreak(node))
});
ordered
}
fn tiebreak(&self, node: &Address) -> u64 {
let mut hash = FNV_OFFSET_BASIS ^ self.salt;
for byte in node.as_ref() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
pub fn set_prediction(&self, prediction: Prediction) {
if let Ok(mut state) = self.state.lock() {
state.prediction = Some(prediction);
state.fetched_at = now_secs();
}
self.dirty.store(true, Ordering::Relaxed);
}
pub fn prediction(&self) -> Option<Prediction> {
self.state.lock().ok().and_then(|state| state.prediction)
}
pub fn prune(&self, known: &[Address]) {
if let Ok(mut state) = self.state.lock() {
let before = state.peers.len();
state.retain_known(known);
if state.peers.len() != before {
self.dirty.store(true, Ordering::Relaxed);
}
}
}
pub fn snapshot(&self) -> Option<BootstrapState> {
self.state.lock().ok().map(|state| state.clone())
}
pub fn flush(&self) {
if !self.dirty.swap(false, Ordering::Relaxed) || !self.store.is_enabled() {
return;
}
let Some(state) = self.snapshot() else {
return;
};
if let Err(error) = self.store.save(&state) {
debug!(%error, "bootstrap cache not written");
}
}
}
impl Drop for Reputation {
fn drop(&mut self) {
self.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
use tape_crypto::hash::Hash;
fn network() -> NetworkKey {
NetworkKey {
program_id: Address::new_unique(),
genesis: Hash([1u8; 32]),
}
}
#[test]
fn preference_order() {
let reputation = Reputation::ephemeral(network());
let fast = Address::new_unique();
let slow = Address::new_unique();
let unknown = Address::new_unique();
let dead = Address::new_unique();
reputation.record_success(fast, Duration::from_millis(10));
reputation.record_success(slow, Duration::from_millis(200));
for _ in 0..3 {
reputation.record_failure(dead);
}
let ordered = reputation.order(&[dead, unknown, slow, fast]);
assert_eq!(ordered[0], fast);
assert_eq!(ordered[1], slow);
assert_eq!(ordered[2], unknown);
assert_eq!(ordered[3], dead, "quarantined peers trail, they are not dropped");
}
#[test]
fn quarantined_demoted() {
let reputation = Reputation::ephemeral(network());
let dead = Address::new_unique();
for _ in 0..3 {
reputation.record_failure(dead);
}
assert!(reputation.is_quarantined(&dead));
let healthy = Address::new_unique();
reputation.record_success(healthy, Duration::from_millis(5));
let ordered = reputation.order(&[dead, healthy]);
assert_eq!(ordered, vec![healthy, dead]);
}
#[test]
fn success_rehabilitates() {
let reputation = Reputation::ephemeral(network());
let flaky = Address::new_unique();
for _ in 0..3 {
reputation.record_failure(flaky);
}
assert!(reputation.is_quarantined(&flaky));
reputation.record_success(flaky, Duration::from_millis(5));
assert!(!reputation.is_quarantined(&flaky));
}
#[test]
fn stable_order() {
let reputation = Reputation::ephemeral(network());
let a = Address::new_unique();
let b = Address::new_unique();
let c = Address::new_unique();
let first = reputation.order(&[a, b, c]);
let second = reputation.order(&[c, b, a]);
assert_eq!(first, second);
}
}