From 19535a851b2a528bcd19f44b73a2d90b46adaa5e Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 9 Nov 2026 03:41:00 +0000 Subject: remake: earlier epochs' metadata read as the snapshot built it A snapshot leaving out earlier epochs' metadata (`snapshot::left_out`) and a rejoining node (`catchup::prepare`) read it from the document as it stands. It is never writable through the store, and under a policy the rules take a change to it only from an epoch's first; but with no policy, screening judges only an epoch's root, so a member's own change could rewrite or remove an entry: a replay window narrowed or widened, a translation or a clock changed, a strict remake's entry gone. Now both read it at the epoch's snapshot change (`snapshot::as_built`), as `verify_document` reads the epoch's own: a snapshot from its base's epoch's, a rejoining node from the target's. Every earlier entry is a copy in that change, so what was written there since doesn't count. The first epoch follows none and has none. --- src/remake/catchup.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'src/remake/catchup.rs') diff --git a/src/remake/catchup.rs b/src/remake/catchup.rs index a3443ba..68fa117 100644 --- a/src/remake/catchup.rs +++ b/src/remake/catchup.rs @@ -520,12 +520,19 @@ fn prepare( Ok(metadata) => metadata, Err(reason) => return Prepared::Failed(reason), }; + // Earlier epochs' metadata as the target's snapshot built it, whatever was written + // there since. + let built = match snapshot::as_built(copy, target) { + Ok(Some(built)) => built, + Ok(None) => return Prepared::Failed(format!("{target} follows no epoch")), + Err(reason) => return Prepared::Failed(reason), + }; // The epoch that followed ours records (per actor) which of our changes it took. // Snapshots carry earlier epochs' metadata forward while a node can replay through // it (`snapshot::left_out`), so this works across any gap within the window; beyond // it, the metadata missing quarantines as the window would. - let next = snapshot::read_epoch_metadata(copy, previous.generation + 1) + let next = snapshot::read_epoch_metadata(&built, previous.generation + 1) .ok() .filter(|next| next.previous == previous); let gap = target.generation - previous.generation; @@ -537,7 +544,7 @@ fn prepare( // this sees every one since our epoch, however far behind. let mut strict = crate::access::policy::of(old).strict(); for generation in previous.generation + 1..=target.generation { - if let Ok(epoch) = snapshot::read_epoch_metadata(copy, generation) + if let Ok(epoch) = snapshot::read_epoch_metadata(&built, generation) && epoch.kind == "strict" { if strict { @@ -552,7 +559,7 @@ fn prepare( let translator = (previous.generation + 1..=target.generation).try_fold( Translator::default(), |mut translator, generation| { - let epoch = snapshot::read_epoch_metadata(copy, generation)?; + let epoch = snapshot::read_epoch_metadata(&built, generation)?; let kind = epoch.kind().ok_or_else(|| { format!( "epoch generation {generation} was made by unknown remake kind {:?}", -- cgit v1.3.1