diff options
| author | tv <tv@krebsco.de> | 2026-11-09 15:40:48 +0000 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-09 18:21:09 +0100 |
| commit | f9bf20fc73095378d67a94592e930fba6dd019f6 (patch) | |
| tree | 0739a5c60971aa49070409b77359a18d0889f0ba /src/remake | |
| parent | e59be7214b1fc1998f76a34f7e4429ee4259494c (diff) | |
remake: a backup's epoch as its snapshot names it
Review 4, appendix: a document loaded from a backup was taken to be on the largest
generation `_continuum/epochs` held as it stood, verified at its only root. In a swarm
without a policy a member's own change could plant `_continuum/epochs/99`, and a backup
then failed to load until it was removed.
The generation is now read at the document's only change without dependencies (its
snapshot writes its own entry and carries the earlier ones), as the rest of the
protocol reads epoch metadata; the genesis change writes none, so a first-epoch backup
is genesis whatever was planted. The test plants one.
Diffstat (limited to 'src/remake')
| -rw-r--r-- | src/remake/snapshot.rs | 43 |
1 files changed, 27 insertions, 16 deletions
diff --git a/src/remake/snapshot.rs b/src/remake/snapshot.rs index b4f1f35..fa1fa39 100644 --- a/src/remake/snapshot.rs +++ b/src/remake/snapshot.rs @@ -251,18 +251,20 @@ pub(crate) fn verify_document(doc: &Document, epoch: Epoch) -> Result<EpochMetad read_epoch_metadata(&at_snapshot, epoch.generation) } -/// The epoch `doc` is on: genesis if it holds no epoch metadata; otherwise the newest -/// generation it has metadata for, whose snapshot change is the document's only change -/// without dependencies (verified). For a document loaded from a backup. +/// The epoch `doc` is on: the newest generation the metadata at its only change without +/// dependencies names (that change, a snapshot, writes its own and carries the earlier +/// ones), verified; genesis if none names one, as the genesis change doesn't. Read at +/// that change, not as the document stands: an entry a member wrote since (with no +/// policy, its own change could) doesn't decide. For a document loaded from a backup. pub(crate) fn epoch_of(doc: &Document) -> Result<Epoch, String> { - let generation = store::keys_at(doc, &meta::path(&[EPOCHS])) - .map_err(|error| error.to_string())? - .unwrap_or_default() - .iter() - .filter_map(|key| key.parse::<u64>().ok()) - .max(); - let Some(generation) = generation.filter(|generation| *generation > 0) else { - return Ok(Epoch::GENESIS); + let generations = |doc: &Document| -> Result<Option<u64>, String> { + Ok(store::keys_at(doc, &meta::path(&[EPOCHS])) + .map_err(|error| error.to_string())? + .unwrap_or_default() + .iter() + .filter_map(|key| key.parse::<u64>().ok()) + .filter(|generation| *generation > 0) + .max()) }; let roots: Vec<ChangeHash> = doc .get_changes(&[]) @@ -271,10 +273,17 @@ pub(crate) fn epoch_of(doc: &Document) -> Result<Epoch, String> { .map(|change| change.hash()) .collect(); let [root] = roots.as_slice() else { - return Err(format!( - "generation {generation} needs exactly one change without dependencies (its snapshot), found {}", - roots.len() - )); + return match generations(doc)? { + None => Ok(Epoch::GENESIS), + Some(generation) => Err(format!( + "generation {generation} needs exactly one change without dependencies (its snapshot), found {}", + roots.len() + )), + }; + }; + let at_root = doc.fork_at(&[*root]).map_err(|error| error.to_string())?; + let Some(generation) = generations(&at_root)? else { + return Ok(Epoch::GENESIS); }; let epoch = Epoch { generation, @@ -690,7 +699,9 @@ mod tests { .fork() .with_actor(ActorId::from(b"actor-b".as_slice())); change(&mut later, |later| { - store::write_path(later, &["k".into()], &json!(2)).unwrap() + store::write_path(later, &["k".into()], &json!(2)).unwrap(); + // A generation planted since doesn't decide. + store::write_path(later, &meta::path(&[EPOCHS, "99"]), &json!({})).unwrap(); }); let backup = Document::load(&later.save()).unwrap(); assert_eq!(epoch_of(&backup), Ok(snap.epoch)); |
