From 9d45ca9c12c89705455eaf7d92fe33e36c5d47d0 Mon Sep 17 00:00:00 2001 From: rymnc <43716372+rymnc@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:01:16 +0530 Subject: [PATCH 1/2] fix(chainfold): reject cursorless snapshots that carry a ring --- crates/chainfold/src/snapshot.rs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/crates/chainfold/src/snapshot.rs b/crates/chainfold/src/snapshot.rs index f46f367..61cc57d 100644 --- a/crates/chainfold/src/snapshot.rs +++ b/crates/chainfold/src/snapshot.rs @@ -199,7 +199,8 @@ pub enum SnapshotError { RingNotAscending, /// Stored hash bytes do not match the ring entry count. RingHashLenMismatch, - /// Stored ring's newest block differs from the stored cursor block. + /// Stored ring's newest block differs from the stored cursor block, or a + /// cursorless snapshot carries ring entries. RingCursorMismatch, /// Caller configuration is invalid. Config(ConfigError), @@ -343,9 +344,8 @@ impl Engine { } let cursor = (envelope.cursor_set != 0) .then(|| Position::new(envelope.cursor_block, envelope.cursor_log_index)); - if let Some(pos) = cursor - && envelope.ring_numbers.last() != Some(&pos.block) - { + // A cursorless engine has an empty ring, so None must pair with no entries. + if envelope.ring_numbers.last().copied() != cursor.map(|pos| pos.block) { return Err(SnapshotError::RingCursorMismatch); } @@ -755,4 +755,21 @@ mod tests { // then RingCursorMismatch assert_eq!(result.unwrap_err(), SnapshotError::RingCursorMismatch); } + + #[test] + fn cursorless_envelope_with_a_ring_is_refused() { + // given a hand-built envelope with no cursor but one observed block + let mut state = Vec::new(); + RecordingFold::default().encode_state(&mut state); + let bytes = encode_custom( + RecordingFold::STATE_TAG.as_bytes(), + None, + &[block(5, 0)], + state, + ); + // when decoded + let result = Engine::::decode_snapshot(&bytes, test_config()); + // then RingCursorMismatch; no engine reaches a cursorless state with a ring + assert_eq!(result.unwrap_err(), SnapshotError::RingCursorMismatch); + } } From d39c213d46debc80a81335d1d52925fe2e09f71f Mon Sep 17 00:00:00 2001 From: rymnc <43716372+rymnc@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:06:02 +0530 Subject: [PATCH 2/2] fix: display --- crates/chainfold/src/snapshot.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/chainfold/src/snapshot.rs b/crates/chainfold/src/snapshot.rs index 61cc57d..4ae37e9 100644 --- a/crates/chainfold/src/snapshot.rs +++ b/crates/chainfold/src/snapshot.rs @@ -247,7 +247,7 @@ impl fmt::Display for SnapshotError { Self::RingCursorMismatch => { write!( f, - "snapshot ring's newest block does not match the cursor block" + "snapshot ring does not end at the cursor block, or has entries without a cursor" ) } Self::Config(error) => write!(f, "snapshot config invalid: {error}"),