1+ package cmd
2+
3+ import (
4+ "fmt"
5+ "strings"
6+
7+ dbm "github.com/cosmos/cosmos-db"
8+ "github.com/crypto-org-chain/cronos/cmd/cronosd/dbmigrate"
9+ "github.com/spf13/cobra"
10+
11+ "github.com/cosmos/cosmos-sdk/client/flags"
12+ "github.com/cosmos/cosmos-sdk/server"
13+ )
14+
15+ const (
16+ flagSourceBackend = "source-backend"
17+ flagTargetBackend = "target-backend"
18+ flagTargetHome = "target-home"
19+ flagBatchSize = "batch-size"
20+ flagVerify = "verify"
21+ flagDBType = "db-type"
22+ flagDatabases = "databases"
23+ flagHeight = "height"
24+ )
25+
26+ // Database type constants
27+ const (
28+ DBTypeApp = "app"
29+ DBTypeCometBFT = "cometbft"
30+ DBTypeAll = "all"
31+ )
32+
33+ // Valid database names
34+ var validDatabaseNames = map [string ]bool {
35+ "application" : true ,
36+ "blockstore" : true ,
37+ "state" : true ,
38+ "tx_index" : true ,
39+ "evidence" : true ,
40+ }
41+
42+ // MigrateDBCmd returns the legacy migrate-db command (for backward compatibility)
43+ func MigrateDBCmd () * cobra.Command {
44+ cmd := & cobra.Command {
45+ Use : "migrate-db" ,
46+ Short : "Migrate databases from one backend to another (e.g., leveldb to rocksdb)" ,
47+ Deprecated : "Use 'database migrate' or 'db migrate' instead" ,
48+ Long : `Migrate databases from one backend to another.
49+
50+ This command migrates databases from a source backend to a target backend.
51+ It can migrate the application database, CometBFT databases, or both.
52+
53+ The migration process:
54+ 1. Opens the source database(s) in read-only mode
55+ 2. Creates new temporary target database(s)
56+ 3. Copies all key-value pairs in batches
57+ 4. Optionally verifies the migration
58+ 5. Creates the target database(s) in a temporary location
59+
60+ Database types (--db-type):
61+ - app: Application database only (application.db)
62+ - cometbft: CometBFT databases only (blockstore.db, state.db, tx_index.db, evidence.db)
63+ - all: Both application and CometBFT databases
64+
65+ Specific databases (--databases):
66+ You can also specify individual databases as a comma-separated list:
67+ - application: Chain state
68+ - blockstore: Block data
69+ - state: Latest state
70+ - tx_index: Transaction indexing
71+ - evidence: Misbehavior evidence
72+
73+ Height Filtering (--height):
74+ For blockstore.db and tx_index.db, you can specify heights to migrate:
75+ - Range: --height 10000-20000 (migrate heights 10000 to 20000)
76+ - Single: --height 123456 (migrate only height 123456)
77+ - Multiple: --height 123456,234567,999999 (migrate specific heights)
78+ - Only applies to blockstore and tx_index databases
79+ - Other databases will ignore height filtering
80+
81+ IMPORTANT:
82+ - Always backup your databases before migration
83+ - The source databases are opened in read-only mode and are not modified
84+ - The target databases are created with a .migrate-temp suffix
85+ - After successful migration, you need to manually replace the original databases
86+ - Stop your node before running this command
87+
88+ Examples:
89+ # Migrate application database only (using --db-type)
90+ cronosd migrate-db --source-backend goleveldb --target-backend rocksdb --db-type app --home ~/.cronos
91+
92+ # Migrate CometBFT databases only (using --db-type)
93+ cronosd migrate-db --source-backend goleveldb --target-backend rocksdb --db-type cometbft --home ~/.cronos
94+
95+ # Migrate all databases (using --db-type)
96+ cronosd migrate-db --source-backend goleveldb --target-backend rocksdb --db-type all --home ~/.cronos
97+
98+ # Migrate specific databases (using --databases)
99+ cronosd migrate-db --source-backend goleveldb --target-backend rocksdb --databases blockstore,tx_index --home ~/.cronos
100+
101+ # Migrate multiple specific databases
102+ cronosd migrate-db --source-backend goleveldb --target-backend rocksdb --databases application,blockstore,state --home ~/.cronos
103+
104+ # Migrate with verification
105+ cronosd migrate-db --source-backend goleveldb --target-backend rocksdb --db-type all --verify --home ~/.cronos
106+
107+ # Migrate blockstore with height range
108+ cronosd migrate-db --source-backend goleveldb --target-backend rocksdb --databases blockstore --height 1000000-2000000 --home ~/.cronos
109+
110+ # Migrate single block height
111+ cronosd migrate-db --source-backend goleveldb --target-backend rocksdb --databases blockstore --height 123456 --home ~/.cronos
112+
113+ # Migrate specific heights
114+ cronosd migrate-db --source-backend goleveldb --target-backend rocksdb --databases blockstore,tx_index --height 100000,200000,300000 --home ~/.cronos
115+ ` ,
116+ RunE : func (cmd * cobra.Command , args []string ) error {
117+ ctx := server .GetServerContextFromCmd (cmd )
118+ logger := ctx .Logger
119+
120+ homeDir := ctx .Viper .GetString (flags .FlagHome )
121+ sourceBackend := ctx .Viper .GetString (flagSourceBackend )
122+ targetBackend := ctx .Viper .GetString (flagTargetBackend )
123+ targetHome := ctx .Viper .GetString (flagTargetHome )
124+ batchSize := ctx .Viper .GetInt (flagBatchSize )
125+ verify := ctx .Viper .GetBool (flagVerify )
126+ dbType := ctx .Viper .GetString (flagDBType )
127+ databases := ctx .Viper .GetString (flagDatabases )
128+ heightFlag := ctx .Viper .GetString (flagHeight )
129+
130+ // Parse backend types
131+ sourceBackendType , err := parseBackendType (sourceBackend )
132+ if err != nil {
133+ return fmt .Errorf ("invalid source backend: %w" , err )
134+ }
135+
136+ targetBackendType , err := parseBackendType (targetBackend )
137+ if err != nil {
138+ return fmt .Errorf ("invalid target backend: %w" , err )
139+ }
140+
141+ if sourceBackendType == targetBackendType {
142+ return fmt .Errorf ("source and target backends must be different" )
143+ }
144+
145+ if targetHome == "" {
146+ targetHome = homeDir
147+ logger .Info ("Target home not specified, using source home directory" , "target_home" , targetHome )
148+ }
149+
150+ // Determine which databases to migrate
151+ var dbNames []string
152+
153+ // If --databases flag is provided, use it (takes precedence over --db-type)
154+ if databases != "" {
155+ var err error
156+ dbNames , err = parseDatabaseNames (databases )
157+ if err != nil {
158+ return err
159+ }
160+ } else {
161+ // Fall back to --db-type flag
162+ var err error
163+ dbNames , err = getDBNamesFromType (dbType )
164+ if err != nil {
165+ return err
166+ }
167+ }
168+
169+ // Parse height flag
170+ heightRange , err := dbmigrate .ParseHeightFlag (heightFlag )
171+ if err != nil {
172+ return fmt .Errorf ("invalid height flag: %w" , err )
173+ }
174+
175+ // Validate height range
176+ if err := heightRange .Validate (); err != nil {
177+ return fmt .Errorf ("invalid height specification: %w" , err )
178+ }
179+
180+ // Warn if height specification is provided but not applicable
181+ if ! heightRange .IsEmpty () {
182+ hasHeightSupport := false
183+ for _ , dbName := range dbNames {
184+ if dbName == "blockstore" || dbName == "tx_index" {
185+ hasHeightSupport = true
186+ break
187+ }
188+ }
189+ if ! hasHeightSupport {
190+ logger .Warn ("Height specification provided but will be ignored (only applies to blockstore and tx_index databases)" ,
191+ "databases" , dbNames ,
192+ "height" , heightRange .String (),
193+ )
194+ }
195+ }
196+
197+ logArgs := []interface {}{
198+ "source_home" , homeDir ,
199+ "target_home" , targetHome ,
200+ "source_backend" , sourceBackend ,
201+ "target_backend" , targetBackend ,
202+ "databases" , dbNames ,
203+ "batch_size" , batchSize ,
204+ "verify" , verify ,
205+ }
206+ if ! heightRange .IsEmpty () {
207+ logArgs = append (logArgs , "height_range" , heightRange .String ())
208+ }
209+ logger .Info ("Database migration configuration" , logArgs ... )
210+
211+ // Prepare RocksDB options if target is RocksDB
212+ var rocksDBOpts interface {}
213+ if targetBackendType == dbm .RocksDBBackend {
214+ // Use the same RocksDB options as the application (implemented in build-tagged files)
215+ rocksDBOpts = dbmigrate .PrepareRocksDBOptions ()
216+ }
217+
218+ // Migrate each database
219+ var totalStats dbmigrate.MigrationStats
220+ for _ , dbName := range dbNames {
221+ logger .Info ("Starting migration" , "database" , dbName )
222+
223+ opts := dbmigrate.MigrateOptions {
224+ SourceHome : homeDir ,
225+ TargetHome : targetHome ,
226+ SourceBackend : sourceBackendType ,
227+ TargetBackend : targetBackendType ,
228+ BatchSize : batchSize ,
229+ Logger : logger ,
230+ RocksDBOptions : rocksDBOpts ,
231+ Verify : verify ,
232+ DBName : dbName ,
233+ HeightRange : heightRange ,
234+ }
235+
236+ stats , err := dbmigrate .Migrate (opts )
237+ if err != nil {
238+ logger .Error ("Migration failed" ,
239+ "database" , dbName ,
240+ "error" , err ,
241+ "processed_keys" , stats .ProcessedKeys .Load (),
242+ "total_keys" , stats .TotalKeys .Load (),
243+ "duration" , stats .Duration (),
244+ )
245+ return fmt .Errorf ("failed to migrate %s: %w" , dbName , err )
246+ }
247+
248+ logger .Info ("Database migration completed" ,
249+ "database" , dbName ,
250+ "total_keys" , stats .TotalKeys .Load (),
251+ "processed_keys" , stats .ProcessedKeys .Load (),
252+ "errors" , stats .ErrorCount .Load (),
253+ "duration" , stats .Duration (),
254+ )
255+
256+ // Accumulate stats
257+ totalStats .TotalKeys .Add (stats .TotalKeys .Load ())
258+ totalStats .ProcessedKeys .Add (stats .ProcessedKeys .Load ())
259+ totalStats .ErrorCount .Add (stats .ErrorCount .Load ())
260+ }
261+
262+ fmt .Println ("\n " + strings .Repeat ("=" , 80 ))
263+ fmt .Println ("ALL MIGRATIONS COMPLETED SUCCESSFULLY" )
264+ fmt .Println (strings .Repeat ("=" , 80 ))
265+ if databases != "" {
266+ fmt .Printf ("Databases: %s\n " , strings .Join (dbNames , ", " ))
267+ } else {
268+ fmt .Printf ("Database Type: %s\n " , dbType )
269+ }
270+ fmt .Printf ("Total Keys: %d\n " , totalStats .TotalKeys .Load ())
271+ fmt .Printf ("Processed Keys: %d\n " , totalStats .ProcessedKeys .Load ())
272+ fmt .Printf ("Errors: %d\n " , totalStats .ErrorCount .Load ())
273+ fmt .Println ("\n IMPORTANT NEXT STEPS:" )
274+ fmt .Println ("1. Backup your original databases" )
275+ fmt .Println ("2. Verify the migration was successful" )
276+ fmt .Println ("3. Migrated databases are located at:" )
277+ for _ , dbName := range dbNames {
278+ fmt .Printf (" %s/data/%s.db.migrate-temp\n " , targetHome , dbName )
279+ }
280+ fmt .Println ("4. Replace the original databases with the migrated ones" )
281+ fmt .Println ("5. Update your config.toml to use the new backend type" )
282+ fmt .Println (strings .Repeat ("=" , 80 ))
283+
284+ return nil
285+ },
286+ }
287+
288+ cmd .Flags ().StringP (flagSourceBackend , "s" , "goleveldb" , "Source database backend type (goleveldb, rocksdb)" )
289+ cmd .Flags ().StringP (flagTargetBackend , "t" , "rocksdb" , "Target database backend type (goleveldb, rocksdb)" )
290+ cmd .Flags ().StringP (flagTargetHome , "o" , "" , "Target home directory (default: same as --home)" )
291+ cmd .Flags ().IntP (flagBatchSize , "b" , dbmigrate .DefaultBatchSize , "Number of key-value pairs to process in a batch" )
292+ cmd .Flags ().BoolP (flagVerify , "v" , true , "Verify migration by comparing source and target databases" )
293+ cmd .Flags ().StringP (flagDBType , "y" , DBTypeApp , "Database type to migrate: app (application.db only), cometbft (CometBFT databases only), all (both)" )
294+ cmd .Flags ().StringP (flagDatabases , "d" , "" , "Comma-separated list of specific databases to migrate (e.g., 'blockstore,tx_index'). Valid names: application, blockstore, state, tx_index, evidence. If specified, this flag takes precedence over --db-type" )
295+ cmd .Flags ().StringP (flagHeight , "H" , "" , "Height specification for blockstore/tx_index: range (10000-20000), single (123456), or multiple (123456,234567,999999). Only applies to blockstore and tx_index databases" )
296+
297+ return cmd
298+ }
299+
300+ // MigrateCmd returns the migrate subcommand for the database command group.
301+ // The returned command is configured with the "migrate" usage and is not marked as deprecated.
302+ func MigrateCmd () * cobra.Command {
303+ cmd := MigrateDBCmd ()
304+ cmd .Use = "migrate"
305+ cmd .Deprecated = ""
306+ return cmd
307+ }
308+
309+ // parseBackendType maps a backend name to the corresponding dbm.BackendType.
310+ // It accepts `goleveldb` or `leveldb` for dbm.GoLevelDBBackend and `rocksdb` for dbm.RocksDBBackend; returns an error for any other value.
311+ func parseBackendType (backend string ) (dbm.BackendType , error ) {
312+ switch backend {
313+ case "goleveldb" , "leveldb" :
314+ return dbm .GoLevelDBBackend , nil
315+ case "rocksdb" :
316+ return dbm .RocksDBBackend , nil
317+ default :
318+ return "" , fmt .Errorf ("unsupported backend type: %s (supported: goleveldb, rocksdb)" , backend )
319+ }
320+ }
321+
322+ // parseDatabaseNames parses and validates a comma-separated list of database names.
323+ // It returns a slice of validated database names in the order provided, or an error if
324+ // the input is empty, contains no valid names, or includes an unsupported name.
325+ func parseDatabaseNames (databases string ) ([]string , error ) {
326+ if databases == "" {
327+ return nil , fmt .Errorf ("no databases specified" )
328+ }
329+
330+ dbList := strings .Split (databases , "," )
331+ var dbNames []string
332+ for _ , dbName := range dbList {
333+ dbName = strings .TrimSpace (dbName )
334+ if dbName == "" {
335+ continue
336+ }
337+ if ! validDatabaseNames [dbName ] {
338+ return nil , fmt .Errorf ("invalid database name: %s (valid names: application, blockstore, state, tx_index, evidence)" , dbName )
339+ }
340+ dbNames = append (dbNames , dbName )
341+ }
342+ if len (dbNames ) == 0 {
343+ return nil , fmt .Errorf ("no valid databases specified in --databases flag" )
344+ }
345+ return dbNames , nil
346+ }
347+
348+ // getDBNamesFromType maps a db-type string to the corresponding list of database names to migrate.
349+ // Supported db-type values are "app", "cometbft", and "all"; any other value returns an error describing valid options.
350+ func getDBNamesFromType (dbType string ) ([]string , error ) {
351+ switch dbType {
352+ case DBTypeApp :
353+ return []string {"application" }, nil
354+ case DBTypeCometBFT :
355+ return []string {"blockstore" , "state" , "tx_index" , "evidence" }, nil
356+ case DBTypeAll :
357+ return []string {"application" , "blockstore" , "state" , "tx_index" , "evidence" }, nil
358+ default :
359+ return nil , fmt .Errorf ("invalid db-type: %s (must be: app, cometbft, or all)" , dbType )
360+ }
361+ }
0 commit comments