Repository: oasisprotocol/oasis-core (go)
Issue: Storage checkpointer doesn't work with non-zero initial version
The storage checkpointer (used by both consensus for state sync and runtimes for checkpoint sync) doesn't work correctly when the initial version (consensus height or runtime round) is non-zero. It will always try to create checkpoints from zero, failing and aborting the checkpoint process. It should instead skip to the earliest available version instead.
Pull request description: go/storage/mkvs/checkpoint: Fix handling of non-zero earliest version
Fixes #3480 

Patch:
diff --git a/.changelog/3480.bugfix.md b/.changelog/3480.bugfix.md
new file mode 100644
index 000000000..94c90f89f
--- /dev/null
+++ b/.changelog/3480.bugfix.md
@@ -0,0 +1,1 @@
+go/storage/mkvs/checkpoint: Fix handling of non-zero earliest version
diff --git a/go/storage/mkvs/checkpoint/checkpointer.go b/go/storage/mkvs/checkpoint/checkpointer.go
index 6462a11a7..b4e2c964f 100644
--- a/go/storage/mkvs/checkpoint/checkpointer.go
+++ b/go/storage/mkvs/checkpoint/checkpointer.go
@@ -156,6 +156,15 @@ func (c *checkpointer) maybeCheckpoint(ctx context.Context, version uint64, para
 	}
 	sort.Slice(cpVersions, func(i, j int) bool { return cpVersions[i] < cpVersions[j] })
 
+	// Make sure to not start earlier than the earliest version.
+	earlyVersion, err := c.ndb.GetEarliestVersion(ctx)
+	if err != nil {
+		return fmt.Errorf("checkpointer: failed to get earliest version: %w", err)
+	}
+	if lastCheckpointVersion < earlyVersion {
+		lastCheckpointVersion = earlyVersion
+	}
+
 	// Checkpoint any missing versions.
 	cpInterval := params.Interval
 	for cpVersion := lastCheckpointVersion + cpInterval; cpVersion < version; cpVersion = cpVersion + cpInterval {


Test patch:
diff --git a/go/storage/mkvs/checkpoint/checkpointer_test.go b/go/storage/mkvs/checkpoint/checkpointer_test.go
index 91c70ddfc..118e94052 100644
--- a/go/storage/mkvs/checkpoint/checkpointer_test.go
+++ b/go/storage/mkvs/checkpoint/checkpointer_test.go
@@ -23,7 +23,7 @@ const (
 	testNumKept       = 2
 )
 
-func TestCheckpointer(t *testing.T) {
+func testCheckpointer(t *testing.T, earliestVersion uint64) {
 	require := require.New(t)
 
 	// Initialize a database.
@@ -60,9 +60,10 @@ func TestCheckpointer(t *testing.T) {
 	// Finalize a few rounds.
 	var root node.Root
 	root.Empty()
+	root.Version = earliestVersion
 	root.Namespace = testNs
 
-	for round := uint64(0); round < 10; round++ {
+	for round := earliestVersion; round < earliestVersion+10; round++ {
 		tree := mkvs.NewWithRoot(nil, ndb, root)
 		err = tree.Insert(ctx, []byte(fmt.Sprintf("round %d", round)), []byte(fmt.Sprintf("value %d", round)))
 		require.NoError(err, "Insert")
@@ -84,7 +85,7 @@ func TestCheckpointer(t *testing.T) {
 		}
 
 		// Make sure that there are always the correct number of checkpoints.
-		if round > testNumKept+1 {
+		if round > earliestVersion+testNumKept+1 {
 			cps, err := fc.GetCheckpoints(ctx, &GetCheckpointsRequest{
 				Version:   checkpointVersion,
 				Namespace: testNs,
@@ -94,3 +95,12 @@ func TestCheckpointer(t *testing.T) {
 		}
 	}
 }
+
+func TestCheckpointer(t *testing.T) {
+	t.Run("Basic", func(t *testing.T) {
+		testCheckpointer(t, 0)
+	})
+	t.Run("NonZeroEarliestVersion", func(t *testing.T) {
+		testCheckpointer(t, 1000)
+	})
+}