aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-09 15:40:48 +0000
committertv <tv@krebsco.de>2026-11-09 18:21:09 +0100
commitf9bf20fc73095378d67a94592e930fba6dd019f6 (patch)
tree0739a5c60971aa49070409b77359a18d0889f0ba
parente59be7214b1fc1998f76a34f7e4429ee4259494c (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.
-rw-r--r--src/remake/snapshot.rs43
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));