diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d750f52f8c..e2b7b4d22a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -46,15 +46,23 @@ jobs: BUILD_TAGS=rocksdb,grocksdb_clean_link,objstore go build -tags $BUILD_TAGS ./cmd/cronosd - golangci-lint run --out-format=github-actions --path-prefix=./ --timeout 30m --build-tags $BUILD_TAGS + golangci-lint run --fix --out-format=github-actions --path-prefix=./ --timeout 30m --build-tags $BUILD_TAGS cd versiondb - golangci-lint run --out-format=github-actions --path-prefix=./versiondb --timeout 30m --build-tags $BUILD_TAGS + golangci-lint run --fix --out-format=github-actions --path-prefix=./versiondb --timeout 30m --build-tags $BUILD_TAGS cd ../memiavl - golangci-lint run --out-format=github-actions --path-prefix=./memiavl --timeout 30m --build-tags objstore + golangci-lint run --fix --out-format=github-actions --path-prefix=./memiavl --timeout 30m --build-tags objstore cd ../store - golangci-lint run --out-format=github-actions --path-prefix=./store --timeout 30m --build-tags objstore + golangci-lint run --fix --out-format=github-actions --path-prefix=./store --timeout 30m --build-tags objstore # Check only if there are differences in the source code if: steps.changed-files.outputs.any_changed == 'true' + - name: check working directory is clean + id: changes + run: | + set +e + (git diff --no-ext-diff --exit-code) + echo "changed=$?" >> $GITHUB_OUTPUT + - if: steps.changes.outputs.changed == 1 + run: echo "Working directory is dirty" && exit 1 lint-python: name: Lint python diff --git a/cmd/cronosd/cmd/dump_root.go b/cmd/cronosd/cmd/dump_root.go new file mode 100644 index 0000000000..ed2e4d07c7 --- /dev/null +++ b/cmd/cronosd/cmd/dump_root.go @@ -0,0 +1,227 @@ +package cmd + +import ( + "bytes" + "fmt" + "sort" + + consensusparamtypes "github.com/cosmos/cosmos-sdk/x/consensus/types" + crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" + ibcfeetypes "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types" + "github.com/crypto-org-chain/cronos/v2/app" + cronostypes "github.com/crypto-org-chain/cronos/v2/x/cronos/types" + e2eetypes "github.com/crypto-org-chain/cronos/v2/x/e2ee/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" + feemarkettypes "github.com/evmos/ethermint/x/feemarket/types" + + dbm "github.com/cosmos/cosmos-db" + capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" + icacontrollertypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/controller/types" + icahosttypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/host/types" + ibctransfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported" + "github.com/crypto-org-chain/cronos/memiavl" + "github.com/spf13/cobra" + + "cosmossdk.io/log" + "cosmossdk.io/store/rootmulti" + "cosmossdk.io/store/types" + evidencetypes "cosmossdk.io/x/evidence/types" + "cosmossdk.io/x/feegrant" + upgradetypes "cosmossdk.io/x/upgrade/types" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +func DumpRootCmd() *cobra.Command { + keys, _, _, _ := app.StoreKeys() + storeNames := make([]string, 0, len(keys)) + for name := range keys { + storeNames = append(storeNames, name) + } + sort.Strings(storeNames) + return DumpRootGroupCmd(storeNames) +} + +func DumpRootGroupCmd(storeNames []string) *cobra.Command { + cmd := &cobra.Command{ + Use: "dump-root", + Short: "dump module root", + } + cmd.AddCommand( + DumpMemIavlRoot(storeNames), + DumpIavlRoot(storeNames), + ) + return cmd +} + +func DumpMemIavlRoot(storeNames []string) *cobra.Command { + cmd := &cobra.Command{ + Use: "dump-memiavl-root [dir]", + Short: "dump mem-iavl root at version [dir]", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + dir := args[0] + version, err := cmd.Flags().GetUint32("version") + if err != nil { + return err + } + opts := memiavl.Options{ + InitialStores: storeNames, + CreateIfMissing: false, + TargetVersion: version, + ReadOnly: true, + } + db, err := memiavl.Load(dir, opts) + if err != nil { + return err + } + defer db.Close() + sort.Strings(storeNames) + for _, storeName := range storeNames { + tree := db.TreeByName(storeName) + if tree != nil { + fmt.Printf("module %s version %d RootHash %X\n", storeName, tree.Version(), tree.RootHash()) + } else { + fmt.Printf("module %s not loaded\n", storeName) + } + } + + db.MultiTree.UpdateCommitInfo() + lastCommitInfo := convertCommitInfo(db.MultiTree.LastCommitInfo()) + + fmt.Printf("Version %d RootHash %X\n", lastCommitInfo.Version, lastCommitInfo.Hash()) + return nil + }, + } + cmd.Flags().Uint32("version", 0, "the version to dump") + return cmd +} + +func convertCommitInfo(commitInfo *memiavl.CommitInfo) *types.CommitInfo { + storeInfos := make([]types.StoreInfo, len(commitInfo.StoreInfos)) + for i, storeInfo := range commitInfo.StoreInfos { + storeInfos[i] = types.StoreInfo{ + Name: storeInfo.Name, + CommitId: types.CommitID{ + Version: storeInfo.CommitId.Version, + Hash: storeInfo.CommitId.Hash, + }, + } + } + return &types.CommitInfo{ + Version: commitInfo.Version, + StoreInfos: storeInfos, + } +} + +func DumpIavlRoot(storeNames []string) *cobra.Command { + // this needs to change in different height + // because some StoreKey version are zero + // such as consensusparamtypes, circuittypes + storeNames = []string{ + authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey, + minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey, + govtypes.StoreKey, paramstypes.StoreKey, upgradetypes.StoreKey, + evidencetypes.StoreKey, capabilitytypes.StoreKey, consensusparamtypes.StoreKey, + feegrant.StoreKey, crisistypes.StoreKey, + // ibc keys + ibcexported.StoreKey, ibctransfertypes.StoreKey, + ibcfeetypes.StoreKey, + // ica keys + icacontrollertypes.StoreKey, + icahosttypes.StoreKey, + // ethermint keys + evmtypes.StoreKey, feemarkettypes.StoreKey, + // e2ee keys + e2eetypes.StoreKey, + // this line is used by starport scaffolding # stargate/app/storeKey + cronostypes.StoreKey, + } + + cmd := &cobra.Command{ + Use: "dump-iavl-root [dir]", + Short: "dump iavl root at version [dir]", + Long: "dump iavl root at version [dir]. To support dumping rocksdb, it should use this https://github.com/cosmos/cosmos-db/blob/9221ee7e2bccf314eff49f89092dd0767588d76e/rocksdb.go#L51.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + dir := args[0] + version, err := cmd.Flags().GetInt64("version") + if err != nil { + return err + } + db, err := dbm.NewGoLevelDB("application", dir, dbm.OptionsMap{"read_only": true}) + if err != nil { + return err + } + defer db.Close() + rs := rootmulti.NewStore(db, log.NewNopLogger(), nil) + for _, storeKey := range storeNames { + rs.MountStoreWithDB(types.NewKVStoreKey(storeKey), types.StoreTypeIAVL, nil) + } + + err = rs.LoadVersion(version) + if err != nil { + fmt.Printf("failed to load version %d %s\n", version, err.Error()) + return err + } + + var cInfo *types.CommitInfo + cInfo, err = rs.GetCommitInfo(version) + if err != nil { + fmt.Printf("failed to load version %d commit info: %s\n", version, err.Error()) + return err + } + infoMaps := make(map[string]types.StoreInfo) + for _, storeInfo := range cInfo.StoreInfos { + infoMaps[storeInfo.Name] = storeInfo + } + + var infos []types.StoreInfo + sort.Strings(storeNames) + for _, storeName := range storeNames { + info, ok := infoMaps[storeName] + if !ok { + fmt.Printf("module %s not loaded\n", storeName) + continue + } + commitID := info.CommitId + fmt.Printf("module %s version %d RootHash %X\n", storeName, commitID.Version, commitID.Hash) + infos = append(infos, info) + } + + if len(infos) != len(cInfo.StoreInfos) { + fmt.Printf("Warning: Partial commit info (loaded %d stores, found %d)\n", len(cInfo.StoreInfos), len(infos)) + storeMaps := make(map[string]struct{}) + for _, storeName := range storeNames { + storeMaps[storeName] = struct{}{} + } + for _, info := range cInfo.StoreInfos { + if _, ok := storeMaps[info.Name]; !ok { + fmt.Printf("module %s missed\n", info.Name) + } + } + } + + commitInfo := &types.CommitInfo{ + Version: version, + StoreInfos: infos, + } + + if rs.LastCommitID().Version != commitInfo.Version || !bytes.Equal(rs.LastCommitID().Hash, commitInfo.Hash()) { + return fmt.Errorf("failed to calculate %d commit info, rs Hash %X, commit Hash %X", rs.LastCommitID().Version, rs.LastCommitID().Hash, commitInfo.Hash()) + } + fmt.Printf("Version %d RootHash %X\n", commitInfo.Version, commitInfo.Hash()) + return nil + }, + } + cmd.Flags().Int64("version", 0, "the version to dump") + return cmd +} diff --git a/cmd/cronosd/cmd/root.go b/cmd/cronosd/cmd/root.go index ec2f5f07e2..5bd9e374a5 100644 --- a/cmd/cronosd/cmd/root.go +++ b/cmd/cronosd/cmd/root.go @@ -185,6 +185,11 @@ func initRootCmd( rootCmd.AddCommand(changeSetCmd) } + dumpRootCmd := DumpRootCmd() + if dumpRootCmd != nil { + rootCmd.AddCommand(dumpRootCmd) + } + // add keybase, auxiliary RPC, query, and tx child commands rootCmd.AddCommand( server.StatusCommand(), diff --git a/memiavl/multitree.go b/memiavl/multitree.go index 5a94e11fb9..f2e2a9b4bb 100644 --- a/memiavl/multitree.go +++ b/memiavl/multitree.go @@ -371,7 +371,8 @@ func (t *MultiTree) WriteSnapshotWithContext(ctx context.Context, dir string, wp } // write the snapshots in parallel and wait all jobs done - group, _ := wp.GroupContext(context.Background()) + // group, _ := wp.GroupContext(context.Background()) + group, _ := wp.GroupContext(ctx) for _, entry := range t.trees { tree, name := entry.Tree, entry.Name diff --git a/memiavl/multitree_test.go b/memiavl/multitree_test.go new file mode 100644 index 0000000000..f0fe7f1351 --- /dev/null +++ b/memiavl/multitree_test.go @@ -0,0 +1,382 @@ +package memiavl + +import ( + "context" + "os" + "path/filepath" + "sync/atomic" + "testing" + "time" + + "github.com/alitto/pond" + "github.com/stretchr/testify/require" +) + +func TestMultiTreeWriteSnapshotWithContextCancellation(t *testing.T) { + mtree := NewEmptyMultiTree(0, 0) + + stores := []string{"store1", "store2", "store3", "store4", "store5"} + var upgrades []*TreeNameUpgrade + for _, name := range stores { + upgrades = append(upgrades, &TreeNameUpgrade{Name: name}) + } + require.NoError(t, mtree.ApplyUpgrades(upgrades)) + + for _, storeName := range stores { + tree := mtree.TreeByName(storeName) + require.NotNil(t, tree) + + for i := 0; i < 1000; i++ { + tree.set([]byte(string(rune('a'+i%26))+string(rune('a'+(i/26)%26))), []byte("value")) + } + } + + _, err := mtree.SaveVersion(true) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + + pool := pond.New(2, 10) + defer pool.StopAndWait() + + snapshotDir := t.TempDir() + + cancel() + + err = mtree.WriteSnapshotWithContext(ctx, snapshotDir, pool) + + require.Error(t, err) + require.ErrorIs(t, err, context.Canceled) +} + +func TestMultiTreeWriteSnapshotWithTimeoutContext(t *testing.T) { + mtree := NewEmptyMultiTree(0, 0) + + stores := []string{"store1", "store2", "store3"} + var upgrades []*TreeNameUpgrade + for _, name := range stores { + upgrades = append(upgrades, &TreeNameUpgrade{Name: name}) + } + require.NoError(t, mtree.ApplyUpgrades(upgrades)) + + for _, storeName := range stores { + tree := mtree.TreeByName(storeName) + require.NotNil(t, tree) + + for i := 0; i < 500; i++ { + tree.set([]byte(string(rune('a'+i%26))+string(rune('a'+(i/26)%26))), []byte("value")) + } + } + + _, err := mtree.SaveVersion(true) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond) + defer cancel() + + time.Sleep(10 * time.Millisecond) + + pool := pond.New(2, 10) + defer pool.StopAndWait() + + snapshotDir := t.TempDir() + + err = mtree.WriteSnapshotWithContext(ctx, snapshotDir, pool) + + require.Error(t, err) + require.ErrorIs(t, err, context.DeadlineExceeded) +} + +func TestMultiTreeWriteSnapshotSuccessWithContext(t *testing.T) { + mtree := NewEmptyMultiTree(0, 0) + + stores := []string{"store1", "store2", "store3"} + var upgrades []*TreeNameUpgrade + for _, name := range stores { + upgrades = append(upgrades, &TreeNameUpgrade{Name: name}) + } + require.NoError(t, mtree.ApplyUpgrades(upgrades)) + + for _, storeName := range stores { + tree := mtree.TreeByName(storeName) + require.NotNil(t, tree) + + for i := 0; i < 100; i++ { + key := []byte(storeName + string(rune('a'+i%26))) + value := []byte("value" + string(rune('0'+i%10))) + tree.set(key, value) + } + } + + _, err := mtree.SaveVersion(true) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + pool := pond.New(4, 10) + defer pool.StopAndWait() + + snapshotDir := t.TempDir() + + err = mtree.WriteSnapshotWithContext(ctx, snapshotDir, pool) + require.NoError(t, err) + + // Verify all stores were written + for _, storeName := range stores { + storeDir := filepath.Join(snapshotDir, storeName) + require.DirExists(t, storeDir) + + // Verify metadata file exists + metadataFile := filepath.Join(storeDir, FileNameMetadata) + require.FileExists(t, metadataFile) + } + + // Verify metadata file was written at root + metadataFile := filepath.Join(snapshotDir, MetadataFileName) + require.FileExists(t, metadataFile) + + // Verify we can load the snapshot back + mtree2, err := LoadMultiTree(snapshotDir, false, 0) + require.NoError(t, err) + defer mtree2.Close() + + require.Equal(t, mtree.Version(), mtree2.Version()) + require.Equal(t, len(mtree.trees), len(mtree2.trees)) + + // Verify data integrity + for _, storeName := range stores { + tree1 := mtree.TreeByName(storeName) + tree2 := mtree2.TreeByName(storeName) + require.NotNil(t, tree1) + require.NotNil(t, tree2) + require.Equal(t, tree1.RootHash(), tree2.RootHash()) + } +} + +func TestMultiTreeWriteSnapshotConcurrentCancellation(t *testing.T) { + mtree := NewEmptyMultiTree(0, 0) + + stores := []string{"store1", "store2", "store3", "store4", "store5", "store6", "store7", "store8"} + var upgrades []*TreeNameUpgrade + for _, name := range stores { + upgrades = append(upgrades, &TreeNameUpgrade{Name: name}) + } + require.NoError(t, mtree.ApplyUpgrades(upgrades)) + + for _, storeName := range stores { + tree := mtree.TreeByName(storeName) + require.NotNil(t, tree) + + for i := 0; i < 2000; i++ { + key := []byte(storeName + string(rune('a'+i%26)) + string(rune('a'+(i/26)%26))) + value := []byte("value" + string(rune('0'+i%10))) + tree.set(key, value) + } + } + + _, err := mtree.SaveVersion(true) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + + pool := pond.New(2, 10) + defer pool.StopAndWait() + + snapshotDir := t.TempDir() + + errChan := make(chan error, 1) + go func() { + errChan <- mtree.WriteSnapshotWithContext(ctx, snapshotDir, pool) + }() + + time.Sleep(5 * time.Millisecond) + cancel() + + err = <-errChan + + // Should return context.Canceled error + require.Error(t, err) + require.ErrorIs(t, err, context.Canceled) + + // Verify that the snapshot directory might be partially written or not at all + // This is acceptable - the important part is that we got the error and stopped + _, statErr := os.Stat(snapshotDir) + if statErr == nil { + // Directory exists, but may be incomplete - this is fine + // The important thing is we stopped and returned an error + t.Logf("this is acceptable") + } +} + +func TestMultiTreeWriteSnapshotEmptyTree(t *testing.T) { + mtree := NewEmptyMultiTree(0, 0) + + stores := []string{"empty1", "empty2"} + var upgrades []*TreeNameUpgrade + for _, name := range stores { + upgrades = append(upgrades, &TreeNameUpgrade{Name: name}) + } + require.NoError(t, mtree.ApplyUpgrades(upgrades)) + + _, err := mtree.SaveVersion(true) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + pool := pond.New(4, 10) + defer pool.StopAndWait() + + snapshotDir := t.TempDir() + + err = mtree.WriteSnapshotWithContext(ctx, snapshotDir, pool) + require.NoError(t, err) + + mtree2, err := LoadMultiTree(snapshotDir, false, 0) + require.NoError(t, err) + defer mtree2.Close() + + require.Equal(t, mtree.Version(), mtree2.Version()) +} + +func TestMultiTreeWriteSnapshotParallelWrites(t *testing.T) { + mtree := NewEmptyMultiTree(0, 0) + + stores := []string{"store1", "store2", "store3", "store4", "store5", "store6", "store7", "store8", "store9", "store10"} + var upgrades []*TreeNameUpgrade + for _, name := range stores { + upgrades = append(upgrades, &TreeNameUpgrade{Name: name}) + } + require.NoError(t, mtree.ApplyUpgrades(upgrades)) + + for _, storeName := range stores { + tree := mtree.TreeByName(storeName) + require.NotNil(t, tree) + + for i := 0; i < 100; i++ { + key := []byte(storeName + string(rune('a'+i%26))) + value := []byte("value" + string(rune('0'+i%10))) + tree.set(key, value) + } + } + + _, err := mtree.SaveVersion(true) + require.NoError(t, err) + + ctx := context.Background() + + poolSizes := []int{1, 2, 4, 8} + for _, poolSize := range poolSizes { + t.Run("PoolSize"+string(rune('0'+poolSize)), func(t *testing.T) { + pool := pond.New(poolSize, poolSize*10) + defer pool.StopAndWait() + + snapshotDir := t.TempDir() + + start := time.Now() + err = mtree.WriteSnapshotWithContext(ctx, snapshotDir, pool) + duration := time.Since(start) + + require.NoError(t, err) + t.Logf("Pool size %d completed in %v", poolSize, duration) + + mtree2, err := LoadMultiTree(snapshotDir, false, 0) + require.NoError(t, err) + defer mtree2.Close() + + require.Equal(t, mtree.Version(), mtree2.Version()) + for _, storeName := range stores { + tree1 := mtree.TreeByName(storeName) + tree2 := mtree2.TreeByName(storeName) + require.Equal(t, tree1.RootHash(), tree2.RootHash()) + } + }) + } +} + +// TestMultiTreeWorkerPoolQueuedTasksShouldNotStart tests that when context is +// canceled, tasks that are queued but haven't started executing should NOT run. +// This test DEMONSTRATES THE BUG at line 374 where context.Background() is used +// instead of the passed ctx, causing all queued tasks to execute even after cancellation. +func TestMultiTreeWorkerPoolQueuedTasksShouldNotStart(t *testing.T) { + mtree := NewEmptyMultiTree(0, 0) + + // Create many stores to ensure tasks will be queued + numStores := 20 + var stores []string + var upgrades []*TreeNameUpgrade + for i := 0; i < numStores; i++ { + name := "store" + string(rune('0'+i%10)) + string(rune('a'+i/10)) + stores = append(stores, name) + upgrades = append(upgrades, &TreeNameUpgrade{Name: name}) + } + require.NoError(t, mtree.ApplyUpgrades(upgrades)) + + // Don't add any data - use empty trees so writeLeaf won't be called + // This means tasks won't check ctx.Done() internally + _, err := mtree.SaveVersion(true) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + + // Create worker pool with only 1 worker but capacity for all tasks + // This ensures most tasks will be queued waiting for the worker + pool := pond.New(1, numStores) + defer pool.StopAndWait() + + // Track how many tasks actually executed + var tasksExecuted atomic.Int32 + + // We need to slow down task execution so we can cancel while tasks are queued + // We'll patch this by checking the execution count after cancellation + + snapshotDir := t.TempDir() + + // Cancel context immediately + cancel() + + // Now call WriteSnapshotWithContext + // BUG: Because line 374 uses context.Background(), the worker pool group + // doesn't know about the cancellation. All 20 tasks will be submitted to the pool. + // With only 1 worker, they'll execute one by one. + + // Since we're using empty trees, tree.WriteSnapshotWithContext doesn't actually + // check ctx (no data to write means no ctx.Done() check in writeLeaf). + // So all tasks will complete successfully despite ctx being canceled. + + err = mtree.WriteSnapshotWithContext(ctx, snapshotDir, pool) + + // With the BUG (context.Background() at line 374): + // - All tasks get queued + // - Worker executes them one by one + // - Empty trees don't trigger context checks + // - Result: err == nil (SUCCESS despite canceled context) + // + // With the FIX (using ctx at line 374): + // - Worker pool's group context would be canceled + // - Queued tasks wouldn't start + // - Result: err == context.Canceled + + if err == nil { + // This proves the bug exists! + t.Logf("BUG REPRODUCED: All %d tasks completed despite canceled context!", numStores) + t.Logf("Tasks executed: %d", tasksExecuted.Load()) + t.Logf("This happens because line 374 uses context.Background() instead of ctx") + + // Verify all stores were actually written (proving tasks ran) + for _, storeName := range stores { + storeDir := filepath.Join(snapshotDir, storeName) + if _, err := os.Stat(storeDir); err == nil { + tasksExecuted.Add(1) + } + } + + t.Logf("Verified: %d stores were written to disk", tasksExecuted.Load()) + t.Fatal("Expected context.Canceled error but got nil - this proves the bug at line 374") + } else { + // If we get here, the bug has been fixed + t.Logf("Bug is FIXED: Got expected error: %v", err) + require.ErrorIs(t, err, context.Canceled) + } +} diff --git a/versiondb/client/cmd.go b/versiondb/client/cmd.go index efc1a00f06..52427bd23d 100644 --- a/versiondb/client/cmd.go +++ b/versiondb/client/cmd.go @@ -29,6 +29,7 @@ func ChangeSetGroupCmd(opts Options) *cobra.Command { RestoreAppDBCmd(opts), RestoreVersionDBCmd(), FixDataCmd(opts.DefaultStores), + DumpVersionDBChangeSetCmd(opts.DefaultStores), ) return cmd } diff --git a/versiondb/client/dump.go b/versiondb/client/dump.go index 8abb5464c7..e898bdb7b9 100644 --- a/versiondb/client/dump.go +++ b/versiondb/client/dump.go @@ -340,3 +340,98 @@ func getFirstVersion(db dbm.DB, iavlVersion int) (int64, error) { return 0, itr.Error() } + +// DumpVersionDBChangeSetCmd only dumps write entries; delete entries cannot be retrieved because RocksDB does not support this. +// Note: Do not use for restore +// it is only used for checking data write errors, such as https://github.com/crypto-org-chain/cronos/issues/1683 +func DumpVersionDBChangeSetCmd(defaultStores []string) *cobra.Command { + cmd := &cobra.Command{ + Use: "dump-versiondb [dir] [outDir]", + Short: "dump versiondb changeset at version [dir] [outDir], don't use for restore", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + dir := args[0] + outDir := args[1] + + startVersion, err := cmd.Flags().GetInt64(flagStartVersion) + if err != nil { + return err + } + endVersion, err := cmd.Flags().GetInt64(flagEndVersion) + if err != nil { + return err + } + versionDB, err := tsrocksdb.NewStore(dir) + if err != nil { + return err + } + if err := os.MkdirAll(outDir, os.ModePerm); err != nil { + return err + } + + stores, err := GetStoresOrDefault(cmd, defaultStores) + if err != nil { + return err + } + + for _, storeKey := range stores { + // Create store directory + storeDir := filepath.Join(outDir, storeKey) + if err := os.MkdirAll(storeDir, os.ModePerm); err != nil { + return err + } + + for version := startVersion; version < endVersion; version++ { + it, err := versionDB.IteratorAtVersion(storeKey, nil, nil, &version) + if err != nil { + return err + } + + fmt.Printf("begin store %s version %d\n", storeKey, version) + // Use same path structure as dump command: //block- + kvsFile := filepath.Join(storeDir, fmt.Sprintf("block-%d", version)) + fpKvs, err := createFile(kvsFile) + if err != nil { + return err + } + kvsWriter := bufio.NewWriter(fpKvs) + + var pairs []*iavl.KVPair + for ; it.Valid(); it.Next() { + if binary.LittleEndian.Uint64(it.Timestamp()) != uint64(version) { + continue + } + key := make([]byte, len(it.Key())) + copy(key, it.Key()) + value := make([]byte, len(it.Value())) + copy(value, it.Value()) + pair := &iavl.KVPair{Key: key, Value: value} + pairs = append(pairs, pair) + } + if err := it.Close(); err != nil { + return err + } + changeset := &iavl.ChangeSet{Pairs: pairs} + err = WriteChangeSet(kvsWriter, version, changeset) + if err != nil { + return err + } + err = kvsWriter.Flush() + if err != nil { + return err + } + err = fpKvs.Close() + if err != nil { + return err + } + fmt.Printf(" Wrote %d changes\n", len(changeset.Pairs)) + } + } + return nil + }, + } + cmd.Flags().Int64(flagStartVersion, 0, "The start version") + cmd.Flags().Int64(flagEndVersion, 0, "The end version, exclusive, default to latestVersion+1") + cmd.Flags().String(flagStores, "", "list of store names, default to the current store list in application") + return cmd +}