use std::cmp;
use std::time::{Duration, SystemTime};
use derive_deftly::Deftly;
use rand::{RngCore, seq::IndexedRandom as _};
use serde::{Deserialize, Serialize};
use tor_basic_utils::RngExt as _;
use tor_error::internal;
use tor_linkspec::{HasRelayIds as _, RelayIdSet, RelayIds};
use tor_netdir::{NetDir, Relay};
use tor_relay_selection::{LowLevelRelayPredicate as _, RelayExclusion, RelaySelector, RelayUsage};
use tor_rtcompat::Runtime;
use tracing::{debug, trace};
#[cfg(test)]
use derive_deftly::derive_deftly_adhoc;
use crate::{VanguardMgrError, VanguardMode};
use super::VanguardParams;
#[derive(Clone, amplify::Getters)]
pub struct Vanguard<'a> {
relay: Relay<'a>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub(crate) struct TimeBoundVanguard {
pub(super) id: RelayIds,
pub(super) when: SystemTime,
}
#[derive(Default, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize)] #[serde(transparent)]
pub(super) struct VanguardSet {
vanguards: Vec<TimeBoundVanguard>,
#[serde(skip)]
target: usize,
}
#[derive(Default, Debug, Clone, PartialEq)] #[derive(Deftly, Serialize, Deserialize)] #[derive_deftly_adhoc]
pub(super) struct VanguardSets {
l2_vanguards: VanguardSet,
l3_vanguards: VanguardSet,
}
impl VanguardSets {
pub(super) fn next_expiry(&self) -> Option<SystemTime> {
let l2_expiry = self.l2_vanguards.next_expiry();
let l3_expiry = self.l3_vanguards.next_expiry();
match (l2_expiry, l3_expiry) {
(Some(e), None) | (None, Some(e)) => Some(e),
(Some(e1), Some(e2)) => Some(cmp::min(e1, e2)),
(None, None) => {
None
}
}
}
pub(super) fn l2(&self) -> &VanguardSet {
&self.l2_vanguards
}
pub(super) fn l3(&self) -> &VanguardSet {
&self.l3_vanguards
}
pub(super) fn remove_expired(&mut self, now: SystemTime) -> usize {
let l2_expired = self.l2_vanguards.remove_expired(now);
let l3_expired = self.l3_vanguards.remove_expired(now);
l2_expired + l3_expired
}
pub(super) fn remove_unlisted(&mut self, netdir: &NetDir) {
self.l2_vanguards.remove_unlisted(netdir);
self.l3_vanguards.remove_unlisted(netdir);
}
pub(super) fn replenish_vanguards<R: Runtime>(
&mut self,
runtime: &R,
netdir: &NetDir,
params: &VanguardParams,
mode: VanguardMode,
) -> Result<(), VanguardMgrError> {
trace!("Replenishing vanguard sets");
self.l2_vanguards.update_target(params.l2_pool_size());
let mut rng = rand::rng();
Self::replenish_set(
runtime,
&mut rng,
netdir,
&mut self.l2_vanguards,
params.l2_lifetime_min(),
params.l2_lifetime_max(),
)?;
if mode == VanguardMode::Full {
self.l3_vanguards.update_target(params.l3_pool_size());
Self::replenish_set(
runtime,
&mut rng,
netdir,
&mut self.l3_vanguards,
params.l3_lifetime_min(),
params.l3_lifetime_max(),
)?;
}
Ok(())
}
fn replenish_set<R: Runtime, Rng: RngCore>(
runtime: &R,
rng: &mut Rng,
netdir: &NetDir,
vanguard_set: &mut VanguardSet,
min_lifetime: Duration,
max_lifetime: Duration,
) -> Result<bool, VanguardMgrError> {
let mut set_changed = false;
let deficit = vanguard_set.deficit();
if deficit > 0 {
let exclude_ids = RelayIdSet::from(&*vanguard_set);
let exclude = RelayExclusion::exclude_identities(exclude_ids);
let new_vanguards = Self::add_n_vanguards(
runtime,
rng,
netdir,
deficit,
exclude,
min_lifetime,
max_lifetime,
)?;
if !new_vanguards.is_empty() {
set_changed = true;
}
for v in new_vanguards {
vanguard_set.add_vanguard(v);
}
}
Ok(set_changed)
}
fn add_n_vanguards<R: Runtime, Rng: RngCore>(
runtime: &R,
rng: &mut Rng,
netdir: &NetDir,
n: usize,
exclude: RelayExclusion,
min_lifetime: Duration,
max_lifetime: Duration,
) -> Result<Vec<TimeBoundVanguard>, VanguardMgrError> {
trace!(relay_count = n, "selecting relays to use as vanguards");
let vanguard_sel = RelaySelector::new(RelayUsage::vanguard(), exclude);
let (relays, _outcome) = vanguard_sel.select_n_relays(rng, n, netdir);
relays
.into_iter()
.map(|relay| {
let duration = select_lifetime(rng, min_lifetime, max_lifetime)?;
let when = runtime.wallclock() + duration;
Ok(TimeBoundVanguard {
id: RelayIds::from_relay_ids(&relay),
when,
})
})
.collect::<Result<Vec<_>, _>>()
}
}
fn select_lifetime<Rng: RngCore>(
rng: &mut Rng,
min_lifetime: Duration,
max_lifetime: Duration,
) -> Result<Duration, VanguardMgrError> {
let err = || internal!("invalid consensus: vanguard min_lifetime > max_lifetime");
let l1 = rng
.gen_range_checked(min_lifetime..=max_lifetime)
.ok_or_else(err)?;
let l2 = rng
.gen_range_checked(min_lifetime..=max_lifetime)
.ok_or_else(err)?;
Ok(std::cmp::max(l1, l2))
}
impl VanguardSet {
pub(super) fn pick_relay<'a, R: RngCore>(
&self,
rng: &mut R,
netdir: &'a NetDir,
relay_selector: &RelaySelector<'a>,
) -> Option<Vanguard<'a>> {
let good_relays = self
.vanguards
.iter()
.filter_map(|vanguard| {
let relay = netdir.by_ids(&vanguard.id)?;
relay_selector
.low_level_predicate_permits_relay(&relay)
.then_some(relay)
})
.collect::<Vec<_>>();
good_relays.choose(rng).map(|relay| Vanguard {
relay: relay.clone(),
})
}
pub(super) fn is_empty(&self) -> bool {
self.vanguards.is_empty()
}
fn deficit(&self) -> usize {
self.target.saturating_sub(self.vanguards.len())
}
fn add_vanguard(&mut self, v: TimeBoundVanguard) {
self.vanguards.push(v);
}
fn remove_unlisted(&mut self, netdir: &NetDir) -> usize {
self.retain(|v| {
let cond = netdir.ids_listed(&v.id) != Some(false);
if !cond {
debug!(id=?v.id, "Removing newly-unlisted vanguard");
}
cond
})
}
fn remove_expired(&mut self, now: SystemTime) -> usize {
self.retain(|v| {
let cond = v.when > now;
if !cond {
debug!(id=?v.id, "Removing expired vanguard");
}
cond
})
}
fn retain<F>(&mut self, f: F) -> usize
where
F: FnMut(&TimeBoundVanguard) -> bool,
{
let old_len = self.vanguards.len();
self.vanguards.retain(f);
old_len - self.vanguards.len()
}
fn next_expiry(&self) -> Option<SystemTime> {
self.vanguards.iter().map(|v| v.when).min()
}
fn update_target(&mut self, target: usize) {
self.target = target;
}
}
impl From<&VanguardSet> for RelayIdSet {
fn from(vanguard_set: &VanguardSet) -> Self {
vanguard_set
.vanguards
.iter()
.flat_map(|vanguard| {
vanguard
.id
.clone()
.identities()
.map(|id| id.to_owned())
.collect::<Vec<_>>()
})
.collect()
}
}
#[cfg(test)]
derive_deftly_adhoc! {
VanguardSets expect items:
impl VanguardSets {
$(
#[doc = concat!("Return the ", stringify!($fname))]
pub(super) fn $fname(&self) -> &Vec<TimeBoundVanguard> {
&self.$fname.vanguards
}
#[doc = concat!("Return the target size of the ", stringify!($fname), " set")]
pub(super) fn $<$fname _target>(&self) -> usize {
self.$fname.target
}
#[doc = concat!("Return the deficit of the ", stringify!($fname), " set")]
pub(super) fn $<$fname _deficit>(&self) -> usize {
self.$fname.deficit()
}
)
}
}