From db8bdbf9245c65cac79178fae731d1569de872ee Mon Sep 17 00:00:00 2001
From: Nick Mathewson <nickm@torproject.org>
Date: Wed, 11 May 2022 12:09:10 -0400
Subject: [PATCH] tor-dirmgr: update status reporting to consider skew
 tolerance

In our status reporting code, we consider an
expired-but-still-usable directory still bootstrapped, but not 100%
bootstrapped.
---
 crates/tor-dirmgr/src/config.rs | 13 ++++-
 crates/tor-dirmgr/src/event.rs  | 89 ++++++++++++++++++++++++---------
 crates/tor-dirmgr/src/state.rs  |  6 +++
 3 files changed, 84 insertions(+), 24 deletions(-)

diff --git a/crates/tor-dirmgr/src/config.rs b/crates/tor-dirmgr/src/config.rs
index a395baa493..d7379d88eb 100644
--- a/crates/tor-dirmgr/src/config.rs
+++ b/crates/tor-dirmgr/src/config.rs
@@ -16,7 +16,7 @@ use tor_checkable::timed::TimerangeBound;
 use tor_config::{define_list_builder_accessors, ConfigBuildError};
 use tor_guardmgr::fallback::FallbackDirBuilder;
 use tor_guardmgr::fallback::FallbackListBuilder;
-use tor_netdoc::doc::netstatus;
+use tor_netdoc::doc::netstatus::{self, Lifetime};
 
 use derive_builder::Builder;
 use serde::{Deserialize, Serialize};
@@ -212,6 +212,17 @@ impl DirSkewTolerance {
             .extend_tolerance(self.post_valid_tolerance)
             .extend_pre_tolerance(self.pre_valid_tolerance)
     }
+
+    /// Return a new consensus [`Lifetime`] that extgends the validity intervals
+    /// of `lifetime` according to this configuration.
+    pub(crate) fn extend_lifetime(&self, lifetime: &Lifetime) -> Lifetime {
+        Lifetime::new(
+            lifetime.valid_after() - self.pre_valid_tolerance,
+            lifetime.fresh_until(),
+            lifetime.valid_until() + self.post_valid_tolerance,
+        )
+        .expect("Logic error when constructing lifetime")
+    }
 }
 
 /// Configuration type for network directory operations.
diff --git a/crates/tor-dirmgr/src/event.rs b/crates/tor-dirmgr/src/event.rs
index 2ce627d31c..0e4ce82129 100644
--- a/crates/tor-dirmgr/src/event.rs
+++ b/crates/tor-dirmgr/src/event.rs
@@ -256,8 +256,11 @@ pub(crate) enum DirStatusInner {
     },
     /// We've downloaded a consensus, but we haven't validated it yet.
     FetchingCerts {
-        /// The lifetime of the consensus.
+        /// The actual declared lifetime of the consensus.
         lifetime: netstatus::Lifetime,
+        /// The lifetime for which we are willing to use this consensus.  (This
+        /// may be broader than `lifetime`.)
+        usable_lifetime: netstatus::Lifetime,
         /// A fraction (in (numerator,denominator) format) of the certificates
         /// we have for this consensus.
         n_certs: (u16, u16),
@@ -265,8 +268,11 @@ pub(crate) enum DirStatusInner {
     /// We've validated a consensus and we're fetching (or have fetched) its
     /// microdescriptors.
     Validated {
-        /// The lifetime of the consensus.
+        /// The actual declared lifetime of the consensus.
         lifetime: netstatus::Lifetime,
+        /// The lifetime for which we are willing to use this consensus.  (This
+        /// may be broader than `lifetime`.)
+        usable_lifetime: netstatus::Lifetime,
         /// A fraction (in (numerator,denominator) form) of the microdescriptors
         /// that we have for this consensus.
         n_mds: (u32, u32),
@@ -359,7 +365,7 @@ impl DirBootstrapStatus {
     /// Return true if this status indicates that we have a current usable
     /// directory.
     pub fn usable_at(&self, now: SystemTime) -> bool {
-        self.current.usable() && self.current.valid_at(now)
+        self.current.usable() && self.current.okay_to_use_at(now)
     }
 
     /// Update this status by replacing its current status (or its next status)
@@ -390,8 +396,8 @@ impl DirBootstrapStatus {
 }
 
 impl DirStatus {
-    /// Return the consensus lifetime for this directory, if we have one.
-    fn lifetime(&self) -> Option<&netstatus::Lifetime> {
+    /// Return the declared consensus lifetime for this directory, if we have one.
+    fn declared_lifetime(&self) -> Option<&netstatus::Lifetime> {
         match &self.0 {
             DirStatusInner::NoConsensus { .. } => None,
             DirStatusInner::FetchingCerts { lifetime, .. } => Some(lifetime),
@@ -399,19 +405,41 @@ impl DirStatus {
         }
     }
 
-    /// Return true if the directory is valid at the given time.
-    fn valid_at(&self, when: SystemTime) -> bool {
-        if let Some(lifetime) = self.lifetime() {
-            lifetime.valid_after() <= when && when < lifetime.valid_until()
-        } else {
-            false
+    /// Return the consensus lifetime for this directory, if we have one, as
+    /// modified by our skew-tolerance settings.
+    fn usable_lifetime(&self) -> Option<&netstatus::Lifetime> {
+        match &self.0 {
+            DirStatusInner::NoConsensus { .. } => None,
+            DirStatusInner::FetchingCerts {
+                usable_lifetime, ..
+            } => Some(usable_lifetime),
+            DirStatusInner::Validated {
+                usable_lifetime, ..
+            } => Some(usable_lifetime),
         }
     }
 
-    /// As frac_at, but return None if this consensus is not valid at the given time.
+    /// Return true if the directory is valid at the given time, as modified by
+    /// our clock skew settings.
+    fn okay_to_use_at(&self, when: SystemTime) -> bool {
+        self.usable_lifetime()
+            .map(|lt| lt.valid_at(when))
+            .unwrap_or(false)
+    }
+
+    /// As `frac`, but return None if this consensus is not valid at the given time,
+    /// and down-rate expired consensuses that we're still willing to use.
     fn frac_at(&self, when: SystemTime) -> Option<f32> {
-        if self.valid_at(when) {
+        if self
+            .declared_lifetime()
+            .map(|lt| lt.valid_at(when))
+            .unwrap_or(false)
+        {
+            // We're officially okay to use this directory.
             Some(self.frac())
+        } else if self.okay_to_use_at(when) {
+            // This directory is a little expired, but only a little.
+            Some(self.frac() * 0.9)
         } else {
             None
         }
@@ -637,20 +665,28 @@ mod test {
         let hour = Duration::new(3600, 0);
 
         let nothing = DirStatus(DirStatusInner::NoConsensus { after: None });
+        let lifetime = netstatus::Lifetime::new(now, now + hour, now + hour * 2).unwrap();
         let unval = DirStatus(DirStatusInner::FetchingCerts {
-            lifetime: netstatus::Lifetime::new(now, now + hour, now + hour * 2).unwrap(),
+            lifetime: lifetime.clone(),
+            usable_lifetime: lifetime,
             n_certs: (3, 5),
         });
+        let lifetime =
+            netstatus::Lifetime::new(now + hour, now + hour * 2, now + hour * 3).unwrap();
         let with_c = DirStatus(DirStatusInner::Validated {
-            lifetime: netstatus::Lifetime::new(now + hour, now + hour * 2, now + hour * 3).unwrap(),
+            lifetime: lifetime.clone(),
+            usable_lifetime: lifetime,
             n_mds: (30, 40),
             usable: false,
         });
 
         // lifetime()
-        assert!(nothing.lifetime().is_none());
-        assert_eq!(unval.lifetime().unwrap().valid_after(), now);
-        assert_eq!(with_c.lifetime().unwrap().valid_until(), now + hour * 3);
+        assert!(nothing.usable_lifetime().is_none());
+        assert_eq!(unval.usable_lifetime().unwrap().valid_after(), now);
+        assert_eq!(
+            with_c.usable_lifetime().unwrap().valid_until(),
+            now + hour * 3
+        );
 
         // at_least_as_new_as()
         assert!(!nothing.at_least_as_new_as(&nothing));
@@ -690,19 +726,22 @@ mod test {
 
         let ds = DirStatus(DirStatusInner::FetchingCerts {
             lifetime: lifetime.clone(),
+            usable_lifetime: lifetime.clone(),
             n_certs: (3, 5),
         });
         assert_eq!(ds.to_string(), "fetching authority certificates (3/5)");
 
         let ds = DirStatus(DirStatusInner::Validated {
             lifetime: lifetime.clone(),
+            usable_lifetime: lifetime.clone(),
             n_mds: (30, 40),
             usable: false,
         });
         assert_eq!(ds.to_string(), "fetching microdescriptors (30/40)");
 
         let ds = DirStatus(DirStatusInner::Validated {
-            lifetime,
+            lifetime: lifetime.clone(),
+            usable_lifetime: lifetime,
             n_mds: (30, 40),
             usable: true,
         });
@@ -722,12 +761,14 @@ mod test {
 
         let ds1: DirStatus = DirStatusInner::Validated {
             lifetime: lifetime.clone(),
+            usable_lifetime: lifetime.clone(),
             n_mds: (3, 40),
             usable: true,
         }
         .into();
         let ds2: DirStatus = DirStatusInner::Validated {
             lifetime: lifetime2.clone(),
+            usable_lifetime: lifetime2.clone(),
             n_mds: (5, 40),
             usable: false,
         }
@@ -756,6 +797,7 @@ mod test {
         let mut bs = bs;
         let ds3 = DirStatus(DirStatusInner::Validated {
             lifetime: lifetime2.clone(),
+            usable_lifetime: lifetime2.clone(),
             n_mds: (10, 40),
             usable: false,
         });
@@ -771,13 +813,14 @@ mod test {
         // Case 2: The new directory _is_ usable and newer.  It will replace the old one.
         let ds4 = DirStatus(DirStatusInner::Validated {
             lifetime: lifetime2.clone(),
+            usable_lifetime: lifetime2.clone(),
             n_mds: (20, 40),
             usable: true,
         });
         bs.update(ds4);
         assert!(bs.next.as_ref().is_none());
         assert_eq!(
-            bs.current.lifetime().unwrap().valid_after(),
+            bs.current.usable_lifetime().unwrap().valid_after(),
             lifetime2.valid_after()
         );
 
@@ -785,15 +828,15 @@ mod test {
         bs.update(ds1);
         assert!(bs.next.as_ref().is_none());
         assert_ne!(
-            bs.current.lifetime().unwrap().valid_after(),
+            bs.current.usable_lifetime().unwrap().valid_after(),
             lifetime.valid_after()
         );
 
         // Case 4: starting with an unusable directory, we always replace.
         let mut bs = DirBootstrapStatus::default();
         assert!(!ds2.usable());
-        assert!(bs.current.lifetime().is_none());
+        assert!(bs.current.usable_lifetime().is_none());
         bs.update(ds2);
-        assert!(bs.current.lifetime().is_some());
+        assert!(bs.current.usable_lifetime().is_some());
     }
 }
diff --git a/crates/tor-dirmgr/src/state.rs b/crates/tor-dirmgr/src/state.rs
index 29f5e014bc..ccb74b90ff 100644
--- a/crates/tor-dirmgr/src/state.rs
+++ b/crates/tor-dirmgr/src/state.rs
@@ -441,6 +441,11 @@ impl<R: Runtime> DirState for GetCertsState<R> {
         let total_certs = n_missing_certs + n_certs;
         DirStatusInner::FetchingCerts {
             lifetime: self.consensus_meta.lifetime().clone(),
+            usable_lifetime: self
+                .config
+                .tolerance
+                .extend_lifetime(self.consensus_meta.lifetime()),
+
             n_certs: (n_certs as u16, total_certs as u16),
         }
         .into()
@@ -841,6 +846,7 @@ impl<R: Runtime> DirState for GetMicrodescsState<R> {
         let n_present = self.n_microdescs - self.partial.n_missing();
         DirStatusInner::Validated {
             lifetime: self.meta.lifetime().clone(),
+            usable_lifetime: self.config.tolerance.extend_lifetime(self.meta.lifetime()),
             n_mds: (n_present as u32, self.n_microdescs as u32),
             usable: self.is_ready(Readiness::Usable),
         }
-- 
GitLab