Create a small go file to interact with the cosmos database
Table of contents
Open Table of contents
Introduction
When it comes to debugging on the Cosmos blockchain, efficiency is key. Picture yourself with your Cosmos-based blockchain, troubleshooting your module state. You’ve prepared your debugger, and your coffee’s within reach, but where do you begin?
Debugging in the Cosmos ecosystem often involves inserting snippets into functions like BeginBlock, to avoid setting up a custom project will all the module declarations defined in app.go
or fighting the database initializations.
That’s where a compact Go file designed specifically for interacting with the specific section of the database related to the module that you are interested in comes into play. With just a few lines of code, you can create a dedicated debugging tool without disrupting your main application.
In this blog post, we’ll create a Go file, integrating it seamlessly into the Cosmos application, and efficiently examining your state for issues.
Considerations
In this tutorial, we assumed that the database backend used is LevelDB
, but the code can be adapted to any other backend.
We are going to use Evmos v16
, so in our go.mod
file we need to replace some dependencies to be able to compile the code, your file should look like this:
module github.com/hanchon/test
go 1.22.2
require (
github.com/cometbft/cometbft-db v0.11.0
github.com/cosmos/cosmos-sdk v0.47.8
github.com/cosmos/iavl v0.21.0-alpha.1.0.20230904092046-df3db2d96583
github.com/evmos/evmos/v16 v16.0.4
)
replace (
github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0
github.com/cosmos/cosmos-sdk => github.com/evmos/cosmos-sdk v0.47.5-evmos.2
github.com/ethereum/go-ethereum => github.com/evmos/go-ethereum v1.10.26-evmos-rc2
github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.9.1
github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
)
Coding
Open the database
To open the database we need the application directory where the data
folder is located and the database backend (in our case GoLevelDB
).
import (
opendb "github.com/evmos/evmos/v16/cmd/evmosd/opendb"
)
func main() {
home, err := os.UserHomeDir()
if err != nil {
log.Fatal(err)
}
db, err := opendb.OpenDB(nil, home+"/.evmosd", dbm.GoLevelDBBackend)
if err != nil {
log.Fatal(err)
}
defer db.Close()
}
Open the module database
Now that we have the LevelDB database object, we need to create the prefixed database for the module that we want to query
import (
...
dbm "github.com/cometbft/cometbft-db"
iavlnew "github.com/cosmos/iavl"
evmtypes "github.com/evmos/evmos/v16/x/evm/types"
)
func OpenModuleDB(db dbm.DB, moduleStoreKey string, chainHeight int64) *iavlnew.MutableTree {
prefixedDb := dbm.NewPrefixDB(db, []byte("s/k:"+moduleStoreKey+"/"))
tree, err := iavlnew.NewMutableTreeWithOpts(
prefixedDb,
0,
&iavlnew.Options{InitialVersion: 0},
true,
)
if err != nil {
log.Fatal(err)
}
_, err = tree.LoadVersion(chainHeight)
if err != nil {
log.Fatal(err)
}
return tree
}
func main(){
...
chainHeight := int64(6)
evmDB := OpenModuleDB(db, evmtypes.StoreKey, chainHeight)
}
NOTE: I am using chain height as 6
for this example because I am running a local node. You should use the latest chain height or a height that was not pruned.
Get all the EVM module entries by wallet
For this example and to avoid handling errors, I am going to make a quick function to panic in case of an error.
import (
...
storeTypes "github.com/cosmos/cosmos-sdk/store/types"
)
func GetIterator(db *iavlnew.MutableTree, start []byte) dbm.Iterator {
it, err := db.Iterator(start, storeTypes.PrefixEndBytes(start), true)
if err != nil {
log.Fatal(err)
}
return it
}
Create the key, you can usually find a way to create the keys using the keys.go
file for the module (x/evm/types/key.go
).
acc, err := sdk.AccAddressFromHexUnsafe("2AB0e9e4eE70FFf1fB9D67031E44F6410170d00e")
if err != nil {
log.Fatal(err)
}
prefix := append(evmtypes.KeyPrefixStorage, acc.Bytes()...)
Now that we have the key, we can iterate over all the storage entries for that wallet
import (
...
"github.com/ethereum/go-ethereum/common"
)
func main(){
...
for ; it.Valid(); it.Next() {
storageKey := common.BytesToHash(it.Key())
storageValue := common.BytesToHash(it.Value())
fmt.Println("storageKey", storageKey, "storageValue", storageValue)
}
}
Putting all together
package main
import (
"fmt"
"log"
"os"
dbm "github.com/cometbft/cometbft-db"
storeTypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
iavlnew "github.com/cosmos/iavl"
"github.com/ethereum/go-ethereum/common"
opendb "github.com/evmos/evmos/v16/cmd/evmosd/opendb"
evmtypes "github.com/evmos/evmos/v16/x/evm/types"
)
func OpenModuleDB(db dbm.DB, moduleStoreKey string, chainHeight int64) *iavlnew.MutableTree {
prefixedDb := dbm.NewPrefixDB(db, []byte("s/k:"+moduleStoreKey+"/"))
tree, err := iavlnew.NewMutableTreeWithOpts(
prefixedDb,
0,
&iavlnew.Options{InitialVersion: 0},
true,
)
if err != nil {
log.Fatal(err)
}
_, err = tree.LoadVersion(chainHeight)
if err != nil {
log.Fatal(err)
}
return tree
}
func GetIterator(db *iavlnew.MutableTree, start []byte) dbm.Iterator {
it, err := db.Iterator(start, storeTypes.PrefixEndBytes(start), true)
if err != nil {
log.Fatal(err)
}
return it
}
func main() {
home, err := os.UserHomeDir()
if err != nil {
log.Fatal(err)
}
db, err := opendb.OpenDB(nil, home+"/.evmosd", dbm.GoLevelDBBackend)
if err != nil {
log.Fatal(err)
}
defer db.Close()
chainHeight := int64(6)
evmDB := OpenModuleDB(db, evmtypes.StoreKey, chainHeight)
acc, err := sdk.AccAddressFromHexUnsafe("2AB0e9e4eE70FFf1fB9D67031E44F6410170d00e")
if err != nil {
log.Fatal(err)
}
prefix := append(evmtypes.KeyPrefixStorage, acc.Bytes()...)
it := GetIterator(evmDB, prefix)
for ; it.Valid(); it.Next() {
storageKey := common.BytesToHash(it.Key())
storageValue := common.BytesToHash(it.Value())
fmt.Println("storageKey", storageKey, "storageValue", storageValue)
}
}
Conclusion
With a small go file, you can read the chain state to gather chain analytics without adding any hacky code to the node.
Accessing directly to the database is useful when you need to make big queries that time out when trying to get that information using the node’s rpc.
I used this method to gather the information related to the XEN token while debugging the state bloat in the Evmos chain, I will publish a detailed explanation about how I did it in the future.