From f9bf20fc73095378d67a94592e930fba6dd019f6 Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 9 Nov 2026 15:40:48 +0000 Subject: 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. --- src/remake/snapshot.rs | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) (limited to 'src') 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 Result { - let generation = store::keys_at(doc, &meta::path(&[EPOCHS])) - .map_err(|error| error.to_string())? - .unwrap_or_default() - .iter() - .filter_map(|key| key.parse::().ok()) - .max(); - let Some(generation) = generation.filter(|generation| *generation > 0) else { - return Ok(Epoch::GENESIS); + let generations = |doc: &Document| -> Result, String> { + Ok(store::keys_at(doc, &meta::path(&[EPOCHS])) + .map_err(|error| error.to_string())? + .unwrap_or_default() + .iter() + .filter_map(|key| key.parse::().ok()) + .filter(|generation| *generation > 0) + .max()) }; let roots: Vec = doc .get_changes(&[]) @@ -271,10 +273,17 @@ pub(crate) fn epoch_of(doc: &Document) -> Result { .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)); -- cgit v1.3.1