From 2e8c7ce33752f0a36b1dd138a2d30bd26327d7e7 Mon Sep 17 00:00:00 2001 From: Robert Pirtle Date: Tue, 28 May 2024 11:00:28 -0700 Subject: [PATCH] feat(cli): add iavlviewer command (#1922) * port iavlviewer to kava v0.26.x to debug app hash * add hash subcommand to iavlviewer additionally, use better error handling * update changelog * separate iavlviewer command into subcommands --------- Co-authored-by: Nick DeLuca --- CHANGELOG.md | 2 + cmd/kava/cmd/iavlviewer/data.go | 53 ++++++++++++++++++ cmd/kava/cmd/iavlviewer/hash.go | 38 +++++++++++++ cmd/kava/cmd/iavlviewer/root.go | 85 +++++++++++++++++++++++++++++ cmd/kava/cmd/iavlviewer/shape.go | 47 ++++++++++++++++ cmd/kava/cmd/iavlviewer/versions.go | 74 +++++++++++++++++++++++++ cmd/kava/cmd/root.go | 2 + go.mod | 6 +- 8 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 cmd/kava/cmd/iavlviewer/data.go create mode 100644 cmd/kava/cmd/iavlviewer/hash.go create mode 100644 cmd/kava/cmd/iavlviewer/root.go create mode 100644 cmd/kava/cmd/iavlviewer/shape.go create mode 100644 cmd/kava/cmd/iavlviewer/versions.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ced6308..a9ccd63a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features - (precisebank) [#1906] Add new `x/precisebank` module with bank decimal extension for EVM usage. +- (cli) [#1922] Add `iavlviewer` CLI command for low-level iavl db debugging. ### Improvements - (rocksdb) [#1903] Bump cometbft-db dependency for use with rocksdb v8.10.0 @@ -336,6 +337,7 @@ the [changelog](https://github.com/cosmos/cosmos-sdk/blob/v0.38.4/CHANGELOG.md). - [#257](https://github.com/Kava-Labs/kava/pulls/257) Include scripts to run large-scale simulations remotely using aws-batch +[#1922]: https://github.com/Kava-Labs/kava/pull/1922 [#1906]: https://github.com/Kava-Labs/kava/pull/1906 [#1903]: https://github.com/Kava-Labs/kava/pull/1903 [#1846]: https://github.com/Kava-Labs/kava/pull/1846 diff --git a/cmd/kava/cmd/iavlviewer/data.go b/cmd/kava/cmd/iavlviewer/data.go new file mode 100644 index 00000000..1098a958 --- /dev/null +++ b/cmd/kava/cmd/iavlviewer/data.go @@ -0,0 +1,53 @@ +package iavlviewer + +import ( + "crypto/sha256" + "fmt" + + "github.com/cosmos/iavl" + ethermintserver "github.com/evmos/ethermint/server" + "github.com/spf13/cobra" +) + +func newDataCmd(opts ethermintserver.StartOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "data [version number]", + Short: "View all keys, hash, & size of tree.", + Args: cobra.RangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + prefix := args[0] + version := 0 + if len(args) == 2 { + var err error + version, err = parseVersion(args[1]) + if err != nil { + return err + } + } + + tree, err := openPrefixTree(opts, cmd, prefix, version) + if err != nil { + return err + } + + printKeys(tree) + hash := tree.Hash() + fmt.Printf("Hash: %X\n", hash) + fmt.Printf("Size: %X\n", tree.Size()) + + return nil + }, + } + + return cmd +} + +func printKeys(tree *iavl.MutableTree) { + fmt.Println("Printing all keys with hashed values (to detect diff)") + tree.Iterate(func(key []byte, value []byte) bool { //nolint:errcheck + printKey := parseWeaveKey(key) + digest := sha256.Sum256(value) + fmt.Printf(" %s\n %X\n", printKey, digest) + return false + }) +} diff --git a/cmd/kava/cmd/iavlviewer/hash.go b/cmd/kava/cmd/iavlviewer/hash.go new file mode 100644 index 00000000..9fd1d379 --- /dev/null +++ b/cmd/kava/cmd/iavlviewer/hash.go @@ -0,0 +1,38 @@ +package iavlviewer + +import ( + "fmt" + + ethermintserver "github.com/evmos/ethermint/server" + "github.com/spf13/cobra" +) + +func newHashCmd(opts ethermintserver.StartOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "hash [version number]", + Short: "Print the root hash of the iavl tree.", + Args: cobra.RangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + prefix := args[0] + version := 0 + if len(args) == 2 { + var err error + version, err = parseVersion(args[1]) + if err != nil { + return err + } + } + + tree, err := openPrefixTree(opts, cmd, prefix, version) + if err != nil { + return err + } + + fmt.Printf("Hash: %X\n", tree.Hash()) + + return nil + }, + } + + return cmd +} diff --git a/cmd/kava/cmd/iavlviewer/root.go b/cmd/kava/cmd/iavlviewer/root.go new file mode 100644 index 00000000..3256c724 --- /dev/null +++ b/cmd/kava/cmd/iavlviewer/root.go @@ -0,0 +1,85 @@ +package iavlviewer + +import ( + "fmt" + "os" + "strconv" + + "cosmossdk.io/log" + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/store/wrapper" + ethermintserver "github.com/evmos/ethermint/server" + "github.com/spf13/cobra" + + "github.com/cosmos/iavl" +) + +const ( + DefaultCacheSize int = 10000 +) + +func NewCmd(opts ethermintserver.StartOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "iavlviewer [version number]", + Short: "Output various data, hashes, and calculations for an iavl tree", + } + + cmd.AddCommand(newDataCmd(opts)) + cmd.AddCommand(newHashCmd(opts)) + cmd.AddCommand(newShapeCmd(opts)) + cmd.AddCommand(newVersionsCmd(opts)) + + return cmd +} + +func parseVersion(arg string) (int, error) { + version, err := strconv.Atoi(arg) + if err != nil { + return 0, fmt.Errorf("invalid version number: '%s'", arg) + } + return version, nil +} + +func openPrefixTree(opts ethermintserver.StartOptions, cmd *cobra.Command, prefix string, version int) (*iavl.MutableTree, error) { + clientCtx := client.GetClientContextFromCmd(cmd) + ctx := server.GetServerContextFromCmd(cmd) + ctx.Config.SetRoot(clientCtx.HomeDir) + + db, err := opts.DBOpener(ctx.Viper, clientCtx.HomeDir, server.GetAppDBBackend(ctx.Viper)) + if err != nil { + return nil, fmt.Errorf("failed to open database at %s: %s", clientCtx.HomeDir, err) + } + defer func() { + if err := db.Close(); err != nil { + ctx.Logger.Error("error closing db", "error", err.Error()) + } + }() + + cosmosdb := wrapper.NewCosmosDB(db) + + tree, err := readTree(cosmosdb, version, []byte(prefix)) + if err != nil { + return nil, fmt.Errorf("failed to read tree with prefix %s: %s", prefix, err) + } + return tree, nil +} + +// ReadTree loads an iavl tree from the directory +// If version is 0, load latest, otherwise, load named version +// The prefix represents which iavl tree you want to read. The iaviwer will always set a prefix. +func readTree(db dbm.DB, version int, prefix []byte) (*iavl.MutableTree, error) { + if len(prefix) != 0 { + db = dbm.NewPrefixDB(db, prefix) + } + + tree := iavl.NewMutableTree(db, DefaultCacheSize, false, log.NewLogger(os.Stdout)) + ver, err := tree.LoadVersion(int64(version)) + if err != nil { + return nil, err + } + fmt.Printf("Latest version: %d\n", ver) + fmt.Printf("Got version: %d\n", version) + return tree, err +} diff --git a/cmd/kava/cmd/iavlviewer/shape.go b/cmd/kava/cmd/iavlviewer/shape.go new file mode 100644 index 00000000..ca0e2658 --- /dev/null +++ b/cmd/kava/cmd/iavlviewer/shape.go @@ -0,0 +1,47 @@ +package iavlviewer + +import ( + "fmt" + "strings" + + "github.com/cosmos/iavl" + ethermintserver "github.com/evmos/ethermint/server" + "github.com/spf13/cobra" +) + +func newShapeCmd(opts ethermintserver.StartOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "shape [version number]", + Short: "View shape of iavl tree.", + Args: cobra.RangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + prefix := args[0] + version := 0 + if len(args) == 2 { + var err error + version, err = parseVersion(args[1]) + if err != nil { + return err + } + } + + tree, err := openPrefixTree(opts, cmd, prefix, version) + if err != nil { + return err + } + + printShape(tree) + + return nil + }, + } + + return cmd +} + +func printShape(tree *iavl.MutableTree) { + // shape := tree.RenderShape(" ", nil) + // TODO: handle this error + shape, _ := tree.RenderShape(" ", nodeEncoder) + fmt.Println(strings.Join(shape, "\n")) +} diff --git a/cmd/kava/cmd/iavlviewer/versions.go b/cmd/kava/cmd/iavlviewer/versions.go new file mode 100644 index 00000000..b4961339 --- /dev/null +++ b/cmd/kava/cmd/iavlviewer/versions.go @@ -0,0 +1,74 @@ +package iavlviewer + +import ( + "bytes" + "encoding/hex" + "fmt" + "strings" + + "github.com/cosmos/iavl" + ethermintserver "github.com/evmos/ethermint/server" + "github.com/spf13/cobra" +) + +func newVersionsCmd(opts ethermintserver.StartOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "versions ", + Short: "Print all versions of iavl tree", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + prefix := args[0] + tree, err := openPrefixTree(opts, cmd, prefix, 15) + if err != nil { + return err + } + + printVersions(tree) + + return nil + }, + } + + return cmd +} + +func printVersions(tree *iavl.MutableTree) { + versions := tree.AvailableVersions() + fmt.Println("Available versions:") + for _, v := range versions { + fmt.Printf(" %d\n", v) + } +} + +// parseWeaveKey assumes a separating : where all in front should be ascii, +// and all afterwards may be ascii or binary +func parseWeaveKey(key []byte) string { + cut := bytes.IndexRune(key, ':') + if cut == -1 { + return encodeID(key) + } + prefix := key[:cut] + id := key[cut+1:] + return fmt.Sprintf("%s:%s", encodeID(prefix), encodeID(id)) +} + +// casts to a string if it is printable ascii, hex-encodes otherwise +func encodeID(id []byte) string { + for _, b := range id { + if b < 0x20 || b >= 0x80 { + return strings.ToUpper(hex.EncodeToString(id)) + } + } + return string(id) +} + +func nodeEncoder(id []byte, depth int, isLeaf bool) string { + prefix := fmt.Sprintf("-%d ", depth) + if isLeaf { + prefix = fmt.Sprintf("*%d ", depth) + } + if len(id) == 0 { + return fmt.Sprintf("%s", prefix) + } + return fmt.Sprintf("%s%s", prefix, parseWeaveKey(id)) +} diff --git a/cmd/kava/cmd/root.go b/cmd/kava/cmd/root.go index 6b9242e7..27074add 100644 --- a/cmd/kava/cmd/root.go +++ b/cmd/kava/cmd/root.go @@ -25,6 +25,7 @@ import ( "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app/params" + "github.com/kava-labs/kava/cmd/kava/cmd/iavlviewer" "github.com/kava-labs/kava/cmd/kava/cmd/rocksdb" "github.com/kava-labs/kava/cmd/kava/opendb" ) @@ -134,5 +135,6 @@ func addSubCmds(rootCmd *cobra.Command, encodingConfig params.EncodingConfig, de keyCommands(app.DefaultNodeHome), rocksdb.RocksDBCmd, newShardCmd(opts), + iavlviewer.NewCmd(opts), ) } diff --git a/go.mod b/go.mod index 735d02b8..625146e0 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,18 @@ go 1.21 require ( cosmossdk.io/errors v1.0.1 + cosmossdk.io/log v1.3.1 cosmossdk.io/math v1.3.0 cosmossdk.io/simapp v0.0.0-20231127212628-044ff4d8c015 github.com/cenkalti/backoff/v4 v4.1.3 github.com/cometbft/cometbft v0.37.4 github.com/cometbft/cometbft-db v0.9.1 + github.com/cosmos/cosmos-db v1.0.0 github.com/cosmos/cosmos-proto v1.0.0-beta.4 github.com/cosmos/cosmos-sdk v0.47.10 github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/gogoproto v1.4.10 + github.com/cosmos/iavl v1.0.0 github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v7 v7.1.3 github.com/cosmos/ibc-go/v7 v7.4.0 github.com/ethereum/go-ethereum v1.10.26 @@ -47,7 +50,6 @@ require ( cosmossdk.io/api v0.3.1 // indirect cosmossdk.io/core v0.6.1 // indirect cosmossdk.io/depinject v1.0.0-alpha.4 // indirect - cosmossdk.io/log v1.3.1 // indirect cosmossdk.io/tools/rosetta v0.2.1 // indirect filippo.io/edwards25519 v1.0.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect @@ -77,9 +79,7 @@ require ( github.com/coinbase/rosetta-sdk-go v0.7.9 // indirect github.com/confio/ics23/go v0.9.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect - github.com/cosmos/cosmos-db v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect - github.com/cosmos/iavl v1.0.0 // indirect github.com/cosmos/ics23/go v0.10.0 // indirect github.com/cosmos/ledger-cosmos-go v0.13.1 // indirect github.com/cosmos/rosetta-sdk-go v0.10.0 // indirect