fetch: Allow upgrading Signed References
After feature levels are introduced to Signed References, and downgrade attack protections are implemented, new errors are encountered when reading Signed References that are in a downgraded state. Such downgrade errors are recoverable by advancing history in a way that restores the feature level that the history requires. However, the implementation in `radicle-fetch` must be adjusted to not abort fetching on downgraded histories, but rather inspect the error in this case and decide whether continuing the fetching process would actually help recover from the downgraded state. The two cases are treated slightly differently for non-delegates and delegates. Since delegates are crucial for evaluating repository identity, also the fact that they cannot recover from a downgraded state is logged at the "info" level.
This commit is contained in:
parent
e245e3115b
commit
81ca4b9eda
|
|
@ -527,18 +527,38 @@ impl FetchState {
|
|||
self.prune(&remote);
|
||||
}
|
||||
(Ok(Some(refs)), false) => {
|
||||
if let Some(SignedRefsAt { at, .. }) =
|
||||
SignedRefsAt::load(remote, handle.repository())?
|
||||
{
|
||||
// Prune non-delegates if they're behind or
|
||||
// diverged. A diverged case is non-fatal for
|
||||
// delegates.
|
||||
if matches!(
|
||||
repository::ancestry(handle.repository(), at, refs.at)?,
|
||||
repository::Ancestry::Behind | repository::Ancestry::Diverged
|
||||
) {
|
||||
self.prune(&remote);
|
||||
continue;
|
||||
let level_reachable = refs.feature_level();
|
||||
|
||||
match SignedRefsAt::load(remote, handle.repository()) {
|
||||
Ok(Some(SignedRefsAt { at, .. })) => {
|
||||
// Prune non-delegates if they're behind or
|
||||
// diverged. A diverged case is non-fatal for
|
||||
// delegates.
|
||||
if matches!(
|
||||
repository::ancestry(handle.repository(), at, refs.at)?,
|
||||
repository::Ancestry::Behind | repository::Ancestry::Diverged
|
||||
) {
|
||||
self.prune(&remote);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(radicle::storage::refs::sigrefs::read::error::Read::Downgrade {
|
||||
levels,
|
||||
actual,
|
||||
..
|
||||
}) => {
|
||||
let level_required = levels.max();
|
||||
if level_reachable >= level_required {
|
||||
log::info!("Non-delegate {remote} has downgraded history, currently stuck at '{actual}', expects to be upgraded to '{level_required}' and will be upgraded to '{level_reachable}'.")
|
||||
} else {
|
||||
log::debug!("Non-delegate {remote} has downgraded history, currently stuck at '{actual}', expects to be upgraded to '{level_required}' but only level '{level_reachable}' was advertised.");
|
||||
self.prune(&remote);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(error::Protocol::Refs(err)),
|
||||
Ok(None) => {
|
||||
// We see signed references for this non-delegate for the first time.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -554,27 +574,46 @@ impl FetchState {
|
|||
}
|
||||
}
|
||||
(Ok(Some(refs)), true) => {
|
||||
if let Some(SignedRefsAt { at, .. }) =
|
||||
SignedRefsAt::load(remote, handle.repository())?
|
||||
{
|
||||
let ancestry = repository::ancestry(handle.repository(), at, refs.at)?;
|
||||
if matches!(ancestry, repository::Ancestry::Behind) {
|
||||
log::trace!(
|
||||
"Advertised `rad/sigrefs` {} is behind {at} for {remote}",
|
||||
refs.at
|
||||
);
|
||||
self.prune(&remote);
|
||||
continue;
|
||||
} else if matches!(ancestry, repository::Ancestry::Diverged) {
|
||||
return Err(error::Protocol::Diverged {
|
||||
remote,
|
||||
current: at,
|
||||
received: refs.at,
|
||||
});
|
||||
let level_reachable = refs.feature_level();
|
||||
|
||||
match SignedRefsAt::load(remote, handle.repository()) {
|
||||
Ok(Some(SignedRefsAt { at, .. })) => {
|
||||
let ancestry = repository::ancestry(handle.repository(), at, refs.at)?;
|
||||
if matches!(ancestry, repository::Ancestry::Behind) {
|
||||
log::trace!(
|
||||
"Advertised `rad/sigrefs` {} is behind {at} for {remote}",
|
||||
refs.at
|
||||
);
|
||||
self.prune(&remote);
|
||||
continue;
|
||||
} else if matches!(ancestry, repository::Ancestry::Diverged) {
|
||||
return Err(error::Protocol::Diverged {
|
||||
remote,
|
||||
current: at,
|
||||
received: refs.at,
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(radicle::storage::refs::sigrefs::read::error::Read::Downgrade {
|
||||
levels,
|
||||
actual,
|
||||
..
|
||||
}) => {
|
||||
let level_required = levels.max();
|
||||
if level_reachable >= level_required {
|
||||
log::info!("Delegate {remote} has downgraded history, currently stuck at '{actual}', expects to be upgraded to '{level_required}' and will be upgraded to '{level_reachable}'.")
|
||||
} else {
|
||||
log::info!("Delegate {remote} has downgraded history, currently stuck at '{actual}', expects to be upgraded to '{level_required}' but only level '{level_reachable}' was advertised.");
|
||||
self.prune(&remote);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(error::Protocol::Refs(err)),
|
||||
Ok(None) => {
|
||||
// We see signed references for this delegate for the first time.
|
||||
}
|
||||
}
|
||||
|
||||
let level = refs.feature_level();
|
||||
let cache = self.as_cached(handle);
|
||||
let mut fails =
|
||||
sigrefs::validate(&cache, refs)?.unwrap_or(Validations::default());
|
||||
|
|
@ -589,8 +628,8 @@ impl FetchState {
|
|||
remotes.insert(remote);
|
||||
}
|
||||
|
||||
if level < FeatureLevel::LATEST {
|
||||
log::warn!("Delegate {remote} is on feature level '{level}' which is lower than '{}', they should consider upgrading Radicle.", FeatureLevel::LATEST)
|
||||
if level_reachable < FeatureLevel::LATEST {
|
||||
log::warn!("Delegate {remote} is on feature level '{level_reachable}' which is lower than '{}', they should consider upgrading Radicle.", FeatureLevel::LATEST)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ impl FeatureLevels {
|
|||
Self(BTreeMap::new())
|
||||
}
|
||||
|
||||
fn max(&self) -> FeatureLevel {
|
||||
pub fn max(&self) -> FeatureLevel {
|
||||
self.0.last_key_value().map(|(k, _)| *k).unwrap_or_default()
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue