From b6bcfaa595d142d4b945bf434350b650bb505874 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Fri, 26 Jun 2026 10:08:25 -0700 Subject: [PATCH 1/4] update checkpoint list tries --- cmd/util/cmd/checkpoint-list-tries/cmd.go | 34 +++++- .../cmd/checkpoint-list-tries/cmd_test.go | 94 +++++++++++++++ cmd/util/cmd/checkpoint-trie-stats/cmd.go | 113 ------------------ 3 files changed, 122 insertions(+), 119 deletions(-) create mode 100644 cmd/util/cmd/checkpoint-list-tries/cmd_test.go delete mode 100644 cmd/util/cmd/checkpoint-trie-stats/cmd.go diff --git a/cmd/util/cmd/checkpoint-list-tries/cmd.go b/cmd/util/cmd/checkpoint-list-tries/cmd.go index 830075bc5c8..a325db37e6b 100644 --- a/cmd/util/cmd/checkpoint-list-tries/cmd.go +++ b/cmd/util/cmd/checkpoint-list-tries/cmd.go @@ -2,10 +2,14 @@ package checkpoint_list_tries import ( "fmt" + "path/filepath" + "strings" + "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/onflow/flow-go/ledger" "github.com/onflow/flow-go/ledger/complete/wal" ) @@ -28,14 +32,32 @@ func init() { func run(*cobra.Command, []string) { - log.Info().Msgf("loading checkpoint %v", flagCheckpoint) - tries, err := wal.LoadCheckpoint(flagCheckpoint, log.Logger) + log.Info().Msgf("reading trie root hashes from checkpoint %v", flagCheckpoint) + + hashes, err := readTrieRootHashes(log.Logger, flagCheckpoint) if err != nil { - log.Fatal().Err(err).Msg("error while loading checkpoint") + log.Fatal().Err(err).Msg("error while reading trie root hashes from checkpoint") + } + log.Info().Msgf("checkpoint read, total tries: %v", len(hashes)) + + for _, h := range hashes { + fmt.Printf("trie root hash: %s\n", h) } - log.Info().Msgf("checkpoint loaded, total tries: %v", len(tries)) +} - for _, trie := range tries { - fmt.Printf("trie root hash: %s\n", trie.RootHash()) +// readTrieRootHashes reads only the trie root hashes from the checkpoint file at +// the given path, without materializing the full trie forest. Only the top-trie +// part file (containing the trie root records) is read. +// +// Both V6 and V7 (payloadless) checkpoints are supported; the version is +// determined by the V7 filename suffix ([wal.V7FileSuffix]). The root hashes are +// returned in the order they are stored in the checkpoint. +// +// No error returns are expected during normal operation. +func readTrieRootHashes(logger zerolog.Logger, checkpointFilePath string) ([]ledger.RootHash, error) { + dir, fileName := filepath.Split(checkpointFilePath) + if strings.HasSuffix(fileName, wal.V7FileSuffix) { + return wal.ReadTriesRootHashV7(logger, dir, fileName) } + return wal.ReadTriesRootHash(logger, dir, fileName) } diff --git a/cmd/util/cmd/checkpoint-list-tries/cmd_test.go b/cmd/util/cmd/checkpoint-list-tries/cmd_test.go new file mode 100644 index 00000000000..138c22d3f07 --- /dev/null +++ b/cmd/util/cmd/checkpoint-list-tries/cmd_test.go @@ -0,0 +1,94 @@ +package checkpoint_list_tries + +import ( + "path/filepath" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/ledger" + "github.com/onflow/flow-go/ledger/common/testutils" + "github.com/onflow/flow-go/ledger/complete/mtrie/trie" + "github.com/onflow/flow-go/ledger/complete/payloadless" + "github.com/onflow/flow-go/ledger/complete/wal" +) + +// TestReadTrieRootHashesV6 verifies that the trie root hashes are read from a V6 +// checkpoint in the order they were stored, without loading the full forest. +func TestReadTrieRootHashesV6(t *testing.T) { + dir := t.TempDir() + const fileName = "checkpoint" + + tries := createV6Tries(t) + + err := wal.StoreCheckpointV6Concurrently(tries, dir, fileName, zerolog.Nop()) + require.NoError(t, err) + + hashes, err := readTrieRootHashes(zerolog.Nop(), filepath.Join(dir, fileName)) + require.NoError(t, err) + + expected := make([]ledger.RootHash, len(tries)) + for i, tr := range tries { + expected[i] = tr.RootHash() + } + require.Equal(t, expected, hashes) +} + +// TestReadTrieRootHashesV7 verifies that the trie root hashes are read from a V7 +// (payloadless) checkpoint in the order they were stored, by dispatching on the +// V7 filename suffix. +func TestReadTrieRootHashesV7(t *testing.T) { + dir := t.TempDir() + fileName := "checkpoint" + wal.V7FileSuffix + + tries := createV7Tries(t) + + err := wal.StoreCheckpointV7Concurrently(tries, dir, fileName, zerolog.Nop()) + require.NoError(t, err) + + hashes, err := readTrieRootHashes(zerolog.Nop(), filepath.Join(dir, fileName)) + require.NoError(t, err) + + expected := make([]ledger.RootHash, len(tries)) + for i, tr := range tries { + expected[i] = tr.RootHash() + } + require.Equal(t, expected, hashes) +} + +// createV6Tries builds a chain of two distinct full-payload tries for use as V6 +// checkpoint content. +func createV6Tries(t *testing.T) []*trie.MTrie { + p1 := testutils.PathByUint8(0) + v1 := testutils.LightPayload8('A', 'a') + trie1, _, err := trie.NewTrieWithUpdatedRegisters( + trie.NewEmptyMTrie(), []ledger.Path{p1}, []ledger.Payload{*v1}, true) + require.NoError(t, err) + + p2 := testutils.PathByUint8(1) + v2 := testutils.LightPayload8('B', 'b') + trie2, _, err := trie.NewTrieWithUpdatedRegisters( + trie1, []ledger.Path{p2}, []ledger.Payload{*v2}, true) + require.NoError(t, err) + + return []*trie.MTrie{trie1, trie2} +} + +// createV7Tries builds a chain of two distinct payloadless tries for use as V7 +// checkpoint content. +func createV7Tries(t *testing.T) []*payloadless.MTrie { + p1 := testutils.PathByUint8(0) + v1 := testutils.LightPayload8('A', 'a') + trie1, _, err := payloadless.NewTrieWithUpdatedRegisters( + payloadless.NewEmptyMTrie(), []ledger.Path{p1}, [][]byte{v1.Value()}, true) + require.NoError(t, err) + + p2 := testutils.PathByUint8(1) + v2 := testutils.LightPayload8('B', 'b') + trie2, _, err := payloadless.NewTrieWithUpdatedRegisters( + trie1, []ledger.Path{p2}, [][]byte{v2.Value()}, true) + require.NoError(t, err) + + return []*payloadless.MTrie{trie1, trie2} +} diff --git a/cmd/util/cmd/checkpoint-trie-stats/cmd.go b/cmd/util/cmd/checkpoint-trie-stats/cmd.go deleted file mode 100644 index 327a4cf037b..00000000000 --- a/cmd/util/cmd/checkpoint-trie-stats/cmd.go +++ /dev/null @@ -1,113 +0,0 @@ -package checkpoint_trie_stats - -import ( - "errors" - "fmt" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - - "github.com/onflow/flow-go/ledger/complete/mtrie/node" - "github.com/onflow/flow-go/ledger/complete/mtrie/trie" - "github.com/onflow/flow-go/ledger/complete/wal" -) - -var ( - flagCheckpoint string - flagTrieIndex int -) - -var Cmd = &cobra.Command{ - Use: "checkpoint-trie-stats", - Short: "List the trie node count by types in a checkpoint, show total payload size", - Run: run, -} - -func init() { - - Cmd.Flags().StringVar(&flagCheckpoint, "checkpoint", "", - "checkpoint file to read") - _ = Cmd.MarkFlagRequired("checkpoint") - Cmd.Flags().IntVar(&flagTrieIndex, "trie-index", 0, "trie index to read, 0 being the first trie, -1 is the last trie") - -} - -func run(*cobra.Command, []string) { - - log.Info().Msgf("loading checkpoint %v, reading %v-th trie", flagCheckpoint, flagTrieIndex) - res, err := scanCheckpoint(flagCheckpoint, flagTrieIndex, log.Logger) - if err != nil { - log.Fatal().Err(err).Msg("fail to scan checkpoint") - } - log.Info(). - Str("TrieRootHash", res.trieRootHash). - Int("InterimNodeCount", res.interimNodeCount). - Int("LeafNodeCount", res.leafNodeCount). - Int("TotalPayloadSize", res.totalPayloadSize). - Msgf("successfully scanned checkpoint %v", flagCheckpoint) -} - -type result struct { - trieRootHash string - interimNodeCount int - leafNodeCount int - totalPayloadSize int -} - -func readTrie(tries []*trie.MTrie, index int) (*trie.MTrie, error) { - if len(tries) == 0 { - return nil, errors.New("No tries available") - } - - if index < -len(tries) || index >= len(tries) { - return nil, fmt.Errorf("index %d out of range", index) - } - - if index < 0 { - return tries[len(tries)+index], nil - } - - return tries[index], nil -} - -func scanCheckpoint(checkpoint string, trieIndex int, log zerolog.Logger) (result, error) { - tries, err := wal.LoadCheckpoint(flagCheckpoint, log) - if err != nil { - return result{}, fmt.Errorf("error while loading checkpoint: %w", err) - } - - log.Info(). - Int("total_tries", len(tries)). - Msg("checkpoint loaded") - - t, err := readTrie(tries, trieIndex) - if err != nil { - return result{}, fmt.Errorf("error while reading trie: %w", err) - } - - log.Info().Msgf("trie loaded, root hash: %v", t.RootHash()) - - res := &result{ - trieRootHash: t.RootHash().String(), - interimNodeCount: 0, - leafNodeCount: 0, - totalPayloadSize: 0, - } - processNode := func(n *node.Node) error { - if n.IsLeaf() { - res.leafNodeCount++ - res.totalPayloadSize += n.Payload().Size() - } else { - res.interimNodeCount++ - } - return nil - } - - err = trie.TraverseNodes(t, processNode) - if err != nil { - return result{}, fmt.Errorf("fail to traverse the trie: %w", err) - } - - return *res, nil -} From f00bd92362709cb20f8588f694d6ecc34b58b22c Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Fri, 26 Jun 2026 10:18:27 -0700 Subject: [PATCH 2/4] checkpoint collect stats only support v6 --- cmd/util/cmd/checkpoint-collect-stats/cmd.go | 37 ++++++++++++ .../cmd/checkpoint-collect-stats/cmd_test.go | 56 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 cmd/util/cmd/checkpoint-collect-stats/cmd_test.go diff --git a/cmd/util/cmd/checkpoint-collect-stats/cmd.go b/cmd/util/cmd/checkpoint-collect-stats/cmd.go index 3269f4914cf..4f116cfe8ae 100644 --- a/cmd/util/cmd/checkpoint-collect-stats/cmd.go +++ b/cmd/util/cmd/checkpoint-collect-stats/cmd.go @@ -3,6 +3,7 @@ package checkpoint_collect_stats import ( "cmp" "encoding/hex" + "fmt" "math" "slices" "strings" @@ -315,6 +316,15 @@ func getPayloadStatsFromCheckpoint(payloadCallBack func(payload *ledger.Payload) memAllocBefore := debug.GetHeapAllocsBytes() log.Info().Msgf("loading checkpoint(s) from %v", flagCheckpointDir) + // checkpoint-collect-stats analyzes payload contents (register types, sizes, + // account info). V7 (payloadless) checkpoints store only leaf hashes and contain + // no payloads, so they cannot be processed here. The WAL replay below loads only + // V6 checkpoints and silently ignores V7 files, which would otherwise produce + // misleading (stale or empty) stats. Fail fast with a clear error instead. + if err := requireV6Checkpoint(flagCheckpointDir); err != nil { + log.Fatal().Err(err).Msg("cannot collect stats from checkpoint") + } + diskWal, err := wal.NewDiskWAL(zerolog.Nop(), nil, &metrics.NoopCollector{}, flagCheckpointDir, complete.DefaultCacheSize, pathfinder.PathByteSize, wal.SegmentSize) if err != nil { log.Fatal().Err(err).Msg("cannot create WAL") @@ -369,6 +379,33 @@ func getPayloadStatsFromCheckpoint(payloadCallBack func(payload *ledger.Payload) return ledgerStats } +// requireV6Checkpoint returns an error if the latest checkpoint in dir is a V7 +// (payloadless) checkpoint. checkpoint-collect-stats requires full payloads, +// which V7 checkpoints do not contain. +// +// Only numbered checkpoints are considered (the WAL bootstrap loads the latest +// numbered V6 checkpoint). If the latest numbered checkpoint is V7, this command +// would otherwise silently fall back to an older V6 checkpoint or an empty state, +// reporting misleading stats. +// +// Expected error returns during normal operation: +// - an error when the latest checkpoint in dir is a V7 (payloadless) checkpoint +func requireV6Checkpoint(dir string) error { + _, latest, err := wal.ListCheckpointsWithInfo(dir) + if err != nil { + return fmt.Errorf("cannot list checkpoints in %s: %w", dir, err) + } + + if latest != nil && latest.Version == wal.VersionV7 { + return fmt.Errorf( + "checkpoint %d in %s is a V7 (payloadless) checkpoint, which contains no payloads; "+ + "checkpoint-collect-stats requires a V6 checkpoint", + latest.Number, dir) + } + + return nil +} + func getRegisterStats(valueSizesByType sizesByType) []RegisterStatsByTypes { domainStats := make([]RegisterStatsByTypes, 0, len(common.AllStorageDomains)) var allDomainSizes []float64 diff --git a/cmd/util/cmd/checkpoint-collect-stats/cmd_test.go b/cmd/util/cmd/checkpoint-collect-stats/cmd_test.go new file mode 100644 index 00000000000..72df37ec599 --- /dev/null +++ b/cmd/util/cmd/checkpoint-collect-stats/cmd_test.go @@ -0,0 +1,56 @@ +package checkpoint_collect_stats + +import ( + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/ledger" + "github.com/onflow/flow-go/ledger/common/testutils" + "github.com/onflow/flow-go/ledger/complete/mtrie/trie" + "github.com/onflow/flow-go/ledger/complete/payloadless" + "github.com/onflow/flow-go/ledger/complete/wal" +) + +// TestRequireV6Checkpoint_EmptyDir verifies that a directory without any numbered +// checkpoint is accepted (the caller proceeds with WAL replay / root checkpoint). +func TestRequireV6Checkpoint_EmptyDir(t *testing.T) { + require.NoError(t, requireV6Checkpoint(t.TempDir())) +} + +// TestRequireV6Checkpoint_V6 verifies that a directory whose latest checkpoint is +// V6 is accepted. +func TestRequireV6Checkpoint_V6(t *testing.T) { + dir := t.TempDir() + + p := testutils.PathByUint8(0) + v := testutils.LightPayload8('A', 'a') + tr, _, err := trie.NewTrieWithUpdatedRegisters( + trie.NewEmptyMTrie(), []ledger.Path{p}, []ledger.Payload{*v}, true) + require.NoError(t, err) + + require.NoError(t, wal.StoreCheckpointV6Concurrently( + []*trie.MTrie{tr}, dir, wal.NumberToFilename(1), zerolog.Nop())) + + require.NoError(t, requireV6Checkpoint(dir)) +} + +// TestRequireV6Checkpoint_V7 verifies that a directory whose latest checkpoint is +// V7 (payloadless) is rejected, since this command requires full payloads. +func TestRequireV6Checkpoint_V7(t *testing.T) { + dir := t.TempDir() + + p := testutils.PathByUint8(0) + v := testutils.LightPayload8('A', 'a') + tr, _, err := payloadless.NewTrieWithUpdatedRegisters( + payloadless.NewEmptyMTrie(), []ledger.Path{p}, [][]byte{v.Value()}, true) + require.NoError(t, err) + + require.NoError(t, wal.StoreCheckpointV7Concurrently( + []*payloadless.MTrie{tr}, dir, wal.NumberToFilenameV7(1), zerolog.Nop())) + + err = requireV6Checkpoint(dir) + require.Error(t, err) + require.Contains(t, err.Error(), "V7") +} From 656258c75ef5bcf02a31999597f041041ad3b537 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:23:35 +0000 Subject: [PATCH 3/4] Fix stale checkpoint-trie-stats import in util root command Co-authored-by: zhangchiqing <811374+zhangchiqing@users.noreply.github.com> --- cmd/util/cmd/root.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/util/cmd/root.go b/cmd/util/cmd/root.go index 308233833ff..591efd1b469 100644 --- a/cmd/util/cmd/root.go +++ b/cmd/util/cmd/root.go @@ -18,7 +18,6 @@ import ( checkpoint_convert_v7 "github.com/onflow/flow-go/cmd/util/cmd/checkpoint-convert-v7" checkpoint_iterate_nodes "github.com/onflow/flow-go/cmd/util/cmd/checkpoint-iterate-nodes" checkpoint_list_tries "github.com/onflow/flow-go/cmd/util/cmd/checkpoint-list-tries" - checkpoint_trie_stats "github.com/onflow/flow-go/cmd/util/cmd/checkpoint-trie-stats" compare_debug_tx "github.com/onflow/flow-go/cmd/util/cmd/compare-debug-tx" db_migration "github.com/onflow/flow-go/cmd/util/cmd/db-migration" debug_script "github.com/onflow/flow-go/cmd/util/cmd/debug-script" @@ -107,7 +106,6 @@ func addCommands() { rootCmd.AddCommand(extract.Cmd) rootCmd.AddCommand(export.Cmd) rootCmd.AddCommand(checkpoint_list_tries.Cmd) - rootCmd.AddCommand(checkpoint_trie_stats.Cmd) rootCmd.AddCommand(checkpoint_collect_stats.Cmd) rootCmd.AddCommand(checkpoint_convert_v7.Cmd) rootCmd.AddCommand(checkpoint_iterate_nodes.Cmd) From 85c94378fad2e7be5455bc3398f10c32fe51760d Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Mon, 14 Sep 2026 09:29:12 -0700 Subject: [PATCH 4/4] address review comments --- cmd/util/cmd/checkpoint-collect-stats/cmd.go | 32 ++++++---- .../cmd/checkpoint-collect-stats/cmd_test.go | 59 +++++++++++++++---- cmd/util/cmd/checkpoint-list-tries/cmd.go | 6 +- cmd/util/common/checkpoint.go | 9 +-- ledger/complete/wal/checkpoint_v6_reader.go | 20 ++++--- 5 files changed, 82 insertions(+), 44 deletions(-) diff --git a/cmd/util/cmd/checkpoint-collect-stats/cmd.go b/cmd/util/cmd/checkpoint-collect-stats/cmd.go index 4f116cfe8ae..cb84ca1e7bd 100644 --- a/cmd/util/cmd/checkpoint-collect-stats/cmd.go +++ b/cmd/util/cmd/checkpoint-collect-stats/cmd.go @@ -379,28 +379,38 @@ func getPayloadStatsFromCheckpoint(payloadCallBack func(payload *ledger.Payload) return ledgerStats } -// requireV6Checkpoint returns an error if the latest checkpoint in dir is a V7 -// (payloadless) checkpoint. checkpoint-collect-stats requires full payloads, -// which V7 checkpoints do not contain. +// requireV6Checkpoint returns an error if the directory's newest checkpoint is a V7 +// (payloadless) checkpoint, i.e. if the newest V7 number is greater than the newest +// V6 number. checkpoint-collect-stats requires full payloads, which V7 checkpoints +// do not contain. // // Only numbered checkpoints are considered (the WAL bootstrap loads the latest -// numbered V6 checkpoint). If the latest numbered checkpoint is V7, this command -// would otherwise silently fall back to an older V6 checkpoint or an empty state, -// reporting misleading stats. +// numbered V6 checkpoint). The two versions are compared per version rather than +// via the combined latest, because a payloadless triedir produced by +// checkpoint-convert-v7 holds both checkpoint.N (V6) and checkpoint.N.v7 for the +// same number. Such a directory is accepted: the WAL replay loads the V6 checkpoint +// and the stats are correct. Only a strictly newer V7 checkpoint would make the +// replay silently fall back to an older V6 checkpoint or an empty state, reporting +// misleading stats. // // Expected error returns during normal operation: -// - an error when the latest checkpoint in dir is a V7 (payloadless) checkpoint +// - an error when the newest checkpoint in dir is a V7 (payloadless) checkpoint func requireV6Checkpoint(dir string) error { - _, latest, err := wal.ListCheckpointsWithInfo(dir) + _, latestV6, err := wal.ListV6Checkpoints(dir) if err != nil { - return fmt.Errorf("cannot list checkpoints in %s: %w", dir, err) + return fmt.Errorf("cannot list V6 checkpoints in %s: %w", dir, err) } - if latest != nil && latest.Version == wal.VersionV7 { + _, latestV7, err := wal.ListV7Checkpoints(dir) + if err != nil { + return fmt.Errorf("cannot list V7 checkpoints in %s: %w", dir, err) + } + + if latestV7 > latestV6 { return fmt.Errorf( "checkpoint %d in %s is a V7 (payloadless) checkpoint, which contains no payloads; "+ "checkpoint-collect-stats requires a V6 checkpoint", - latest.Number, dir) + latestV7, dir) } return nil diff --git a/cmd/util/cmd/checkpoint-collect-stats/cmd_test.go b/cmd/util/cmd/checkpoint-collect-stats/cmd_test.go index 72df37ec599..2007b21845d 100644 --- a/cmd/util/cmd/checkpoint-collect-stats/cmd_test.go +++ b/cmd/util/cmd/checkpoint-collect-stats/cmd_test.go @@ -23,7 +23,48 @@ func TestRequireV6Checkpoint_EmptyDir(t *testing.T) { // V6 is accepted. func TestRequireV6Checkpoint_V6(t *testing.T) { dir := t.TempDir() + storeV6Checkpoint(t, dir, 1) + require.NoError(t, requireV6Checkpoint(dir)) +} + +// TestRequireV6Checkpoint_V7 verifies that a directory whose latest checkpoint is +// V7 (payloadless) is rejected, since this command requires full payloads. +func TestRequireV6Checkpoint_V7(t *testing.T) { + dir := t.TempDir() + storeV7Checkpoint(t, dir, 1) + + err := requireV6Checkpoint(dir) + require.Error(t, err) + require.Contains(t, err.Error(), "V7") +} + +// TestRequireV6Checkpoint_V6AndV7SameNumber verifies that a payloadless triedir +// holding both checkpoint.N (V6) and checkpoint.N.v7 for the same number is +// accepted: the WAL replay loads the V6 checkpoint, so the stats are correct. +func TestRequireV6Checkpoint_V6AndV7SameNumber(t *testing.T) { + dir := t.TempDir() + storeV6Checkpoint(t, dir, 1) + storeV7Checkpoint(t, dir, 1) + + require.NoError(t, requireV6Checkpoint(dir)) +} + +// TestRequireV6Checkpoint_V7NewerThanV6 verifies that a strictly newer V7 +// checkpoint is rejected even when older V6 checkpoints exist, since the WAL replay +// would silently fall back to an older V6 checkpoint and report stale stats. +func TestRequireV6Checkpoint_V7NewerThanV6(t *testing.T) { + dir := t.TempDir() + storeV6Checkpoint(t, dir, 1) + storeV7Checkpoint(t, dir, 2) + + err := requireV6Checkpoint(dir) + require.Error(t, err) + require.Contains(t, err.Error(), "V7") +} + +// storeV6Checkpoint writes a single-trie V6 checkpoint numbered `number` into dir. +func storeV6Checkpoint(t *testing.T, dir string, number int) { p := testutils.PathByUint8(0) v := testutils.LightPayload8('A', 'a') tr, _, err := trie.NewTrieWithUpdatedRegisters( @@ -31,16 +72,12 @@ func TestRequireV6Checkpoint_V6(t *testing.T) { require.NoError(t, err) require.NoError(t, wal.StoreCheckpointV6Concurrently( - []*trie.MTrie{tr}, dir, wal.NumberToFilename(1), zerolog.Nop())) - - require.NoError(t, requireV6Checkpoint(dir)) + []*trie.MTrie{tr}, dir, wal.NumberToFilename(number), zerolog.Nop())) } -// TestRequireV6Checkpoint_V7 verifies that a directory whose latest checkpoint is -// V7 (payloadless) is rejected, since this command requires full payloads. -func TestRequireV6Checkpoint_V7(t *testing.T) { - dir := t.TempDir() - +// storeV7Checkpoint writes a single-trie V7 (payloadless) checkpoint numbered +// `number` into dir. +func storeV7Checkpoint(t *testing.T, dir string, number int) { p := testutils.PathByUint8(0) v := testutils.LightPayload8('A', 'a') tr, _, err := payloadless.NewTrieWithUpdatedRegisters( @@ -48,9 +85,5 @@ func TestRequireV6Checkpoint_V7(t *testing.T) { require.NoError(t, err) require.NoError(t, wal.StoreCheckpointV7Concurrently( - []*payloadless.MTrie{tr}, dir, wal.NumberToFilenameV7(1), zerolog.Nop())) - - err = requireV6Checkpoint(dir) - require.Error(t, err) - require.Contains(t, err.Error(), "V7") + []*payloadless.MTrie{tr}, dir, wal.NumberToFilenameV7(number), zerolog.Nop())) } diff --git a/cmd/util/cmd/checkpoint-list-tries/cmd.go b/cmd/util/cmd/checkpoint-list-tries/cmd.go index a325db37e6b..2dba98237b3 100644 --- a/cmd/util/cmd/checkpoint-list-tries/cmd.go +++ b/cmd/util/cmd/checkpoint-list-tries/cmd.go @@ -3,7 +3,6 @@ package checkpoint_list_tries import ( "fmt" "path/filepath" - "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -56,8 +55,5 @@ func run(*cobra.Command, []string) { // No error returns are expected during normal operation. func readTrieRootHashes(logger zerolog.Logger, checkpointFilePath string) ([]ledger.RootHash, error) { dir, fileName := filepath.Split(checkpointFilePath) - if strings.HasSuffix(fileName, wal.V7FileSuffix) { - return wal.ReadTriesRootHashV7(logger, dir, fileName) - } - return wal.ReadTriesRootHash(logger, dir, fileName) + return wal.ReadCheckpointTriesRootHash(logger, dir, fileName) } diff --git a/cmd/util/common/checkpoint.go b/cmd/util/common/checkpoint.go index f5b7e4cfbd1..b884dd7d69a 100644 --- a/cmd/util/common/checkpoint.go +++ b/cmd/util/common/checkpoint.go @@ -3,7 +3,6 @@ package common import ( "fmt" "path/filepath" - "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -34,13 +33,7 @@ func FindHeightsByCheckpoints( // find all trie root hashes in the checkpoint file dir, fileName := filepath.Split(checkpointFilePath) - var hashes []ledger.RootHash - var err error - if strings.HasSuffix(fileName, wal.V7FileSuffix) { - hashes, err = wal.ReadTriesRootHashV7(logger, dir, fileName) - } else { - hashes, err = wal.ReadTriesRootHash(logger, dir, fileName) - } + hashes, err := wal.ReadCheckpointTriesRootHash(logger, dir, fileName) if err != nil { return 0, flow.DummyStateCommitment, 0, fmt.Errorf("could not read trie root hashes from checkpoint file %v: %w", diff --git a/ledger/complete/wal/checkpoint_v6_reader.go b/ledger/complete/wal/checkpoint_v6_reader.go index 1f6ed7a6b2c..d51c1ce0e81 100644 --- a/ledger/complete/wal/checkpoint_v6_reader.go +++ b/ledger/complete/wal/checkpoint_v6_reader.go @@ -703,11 +703,17 @@ func readTriesRootHash(logger zerolog.Logger, dir string, fileName string) ( return trieRootsToReturn, errToReturn } -// readCheckpointTriesRootHash reads the trie root hashes from either a V6 or V7 -// checkpoint, dispatching by the [V7FileSuffix] on filename. Callers that already -// know which version they want should call [ReadTriesRootHash] or -// [ReadTriesRootHashV7] directly. -func readCheckpointTriesRootHash(logger zerolog.Logger, dir, fileName string) ([]ledger.RootHash, error) { +// ReadCheckpointTriesRootHash reads the trie root hashes from either a V6 or V7 +// (payloadless) checkpoint, dispatching by the [V7FileSuffix] on `fileName`. +// +// Both checkpoint versions may coexist in the same directory, so callers that only +// have a directory and filename (and not the version) should use this function +// rather than dispatching on the suffix themselves. Callers that already know which +// version they want should call [ReadTriesRootHash] or [ReadTriesRootHashV7] +// directly. +// +// No error returns are expected during normal operation. +func ReadCheckpointTriesRootHash(logger zerolog.Logger, dir, fileName string) ([]ledger.RootHash, error) { if strings.HasSuffix(fileName, V7FileSuffix) { return ReadTriesRootHashV7(logger, dir, fileName) } @@ -716,7 +722,7 @@ func readCheckpointTriesRootHash(logger zerolog.Logger, dir, fileName string) ([ // checkpointHasRootHash check if the given checkpoint file contains the expected root hash func checkpointHasRootHash(logger zerolog.Logger, bootstrapDir, filename string, expectedRootHash ledger.RootHash) error { - roots, err := readCheckpointTriesRootHash(logger, bootstrapDir, filename) + roots, err := ReadCheckpointTriesRootHash(logger, bootstrapDir, filename) if err != nil { return fmt.Errorf("could not read checkpoint root hash: %w", err) } @@ -738,7 +744,7 @@ func checkpointHasRootHash(logger zerolog.Logger, bootstrapDir, filename string, } func checkpointHasSingleRootHash(logger zerolog.Logger, bootstrapDir, filename string, expectedRootHash ledger.RootHash) error { - roots, err := readCheckpointTriesRootHash(logger, bootstrapDir, filename) + roots, err := ReadCheckpointTriesRootHash(logger, bootstrapDir, filename) if err != nil { return fmt.Errorf("could not read checkpoint root hash: %w", err) }