diff --git a/CHANGELOG.md b/CHANGELOG.md index b052ebd3..43a7a204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,9 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +### Features +- (cli) [#1922] Add `iavlviewer` CLI command for low-level iavl db debugging. + ### Improvements - (pricefeed) [#1851] optimize EndBlocker to iterate all markets only once - (cdp) [#1822] optimize BeginBlocker by removing unnecessary/redundant checks & calculations @@ -334,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 [#1851]: https://github.com/Kava-Labs/kava/pull/1851 [#1846]: https://github.com/Kava-Labs/kava/pull/1846 [#1848]: https://github.com/Kava-Labs/kava/pull/1848 diff --git a/cmd/kava/cmd/iavlviewer/data.go b/cmd/kava/cmd/iavlviewer/data.go new file mode 100644 index 00000000..6dd63c76 --- /dev/null +++ b/cmd/kava/cmd/iavlviewer/data.go @@ -0,0 +1,56 @@ +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, err := tree.Hash() + if err != nil { + return err + } + 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..7e411d32 --- /dev/null +++ b/cmd/kava/cmd/iavlviewer/hash.go @@ -0,0 +1,42 @@ +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 + } + + hash, err := tree.Hash() + if err != nil { + return err + } + fmt.Printf("Hash: %X\n", 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..822104bd --- /dev/null +++ b/cmd/kava/cmd/iavlviewer/root.go @@ -0,0 +1,83 @@ +package iavlviewer + +import ( + "fmt" + "strconv" + + dbm "github.com/cometbft/cometbft-db" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server" + 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()) + } + }() + + tree, err := readTree(db, 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, err := iavl.NewMutableTree(db, DefaultCacheSize, false) + if err != nil { + return nil, err + } + 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 dee72d69..78251d12 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( 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 v0.20.1 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 @@ -76,7 +77,6 @@ require ( github.com/confio/ics23/go v0.9.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect - github.com/cosmos/iavl v0.20.1 // 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