diff --git a/runtime/store.go b/runtime/store.go index a6a2866fbeb3..298582087caf 100644 --- a/runtime/store.go +++ b/runtime/store.go @@ -2,6 +2,8 @@ package runtime import ( "context" + "errors" + "fmt" "io" "cosmossdk.io/core/store" @@ -184,6 +186,11 @@ func KVStoreAdapter(store store.KVStore) storetypes.KVStore { // UpgradeStoreLoader is used to prepare baseapp with a fixed StoreLoader // pattern. This is useful for custom upgrade loading logic. func UpgradeStoreLoader(upgradeHeight int64, storeUpgrades *store.StoreUpgrades) baseapp.StoreLoader { + // sanity checks on store upgrades + if err := checkStoreUpgrade(storeUpgrades); err != nil { + panic(err) + } + return func(ms storetypes.CommitMultiStore) error { if upgradeHeight == ms.LastCommitID().Version+1 { // Check if the current commit version and upgrade height matches @@ -200,3 +207,38 @@ func UpgradeStoreLoader(upgradeHeight int64, storeUpgrades *store.StoreUpgrades) return baseapp.DefaultStoreLoader(ms) } } + +// checkStoreUpgrade performs sanity checks on the store upgrades +func checkStoreUpgrade(storeUpgrades *store.StoreUpgrades) error { + if storeUpgrades == nil { + return errors.New("store upgrades cannot be nil") + } + + // check for duplicates + exists := make(map[string]bool) + for _, key := range storeUpgrades.Added { + if exists[key] { + return fmt.Errorf("store upgrade has duplicate key %s in added", key) + } + + if storeUpgrades.IsDeleted(key) { + return fmt.Errorf("store upgrade has key %s in both added and deleted", key) + } + + exists[key] = true + } + exists = make(map[string]bool) + for _, key := range storeUpgrades.Deleted { + if exists[key] { + return fmt.Errorf("store upgrade has duplicate key %s in deleted", key) + } + + if storeUpgrades.IsAdded(key) { + return fmt.Errorf("store upgrade has key %s in both added and deleted", key) + } + + exists[key] = true + } + + return nil +} diff --git a/runtime/store_test.go b/runtime/store_test.go new file mode 100644 index 000000000000..a583a08d0391 --- /dev/null +++ b/runtime/store_test.go @@ -0,0 +1,65 @@ +package runtime + +import ( + "testing" + + "github.com/stretchr/testify/require" + + corestore "cosmossdk.io/core/store" +) + +func TestCheckStoreUpgrade(t *testing.T) { + tests := []struct { + name string + storeUpgrades *corestore.StoreUpgrades + errMsg string + }{ + { + name: "Nil StoreUpgrades", + storeUpgrades: nil, + errMsg: "store upgrades cannot be nil", + }, + { + name: "Valid StoreUpgrades", + storeUpgrades: &corestore.StoreUpgrades{ + Added: []string{"store1", "store2"}, + Deleted: []string{"store3", "store4"}, + }, + }, + { + name: "Duplicate key in Added", + storeUpgrades: &corestore.StoreUpgrades{ + Added: []string{"store1", "store2", "store1"}, + Deleted: []string{"store3"}, + }, + errMsg: "store upgrade has duplicate key store1 in added", + }, + { + name: "Duplicate key in Deleted", + storeUpgrades: &corestore.StoreUpgrades{ + Added: []string{"store1"}, + Deleted: []string{"store2", "store3", "store2"}, + }, + errMsg: "store upgrade has duplicate key store2 in deleted", + }, + { + name: "Key in both Added and Deleted", + storeUpgrades: &corestore.StoreUpgrades{ + Added: []string{"store1", "store2"}, + Deleted: []string{"store2", "store3"}, + }, + errMsg: "store upgrade has key store2 in both added and deleted", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := checkStoreUpgrade(tt.storeUpgrades) + if tt.errMsg == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tt.errMsg) + } + }) + } +} diff --git a/runtime/v2/store.go b/runtime/v2/store.go index 5268033ad323..de05a27dfd12 100644 --- a/runtime/v2/store.go +++ b/runtime/v2/store.go @@ -1,6 +1,7 @@ package runtime import ( + "errors" "fmt" "cosmossdk.io/core/store" @@ -69,6 +70,11 @@ func DefaultStoreLoader(store Store) error { // UpgradeStoreLoader upgrades the store if the upgrade height matches the current version, it is used as a replacement // for the DefaultStoreLoader when there are store upgrades func UpgradeStoreLoader(upgradeHeight int64, storeUpgrades *store.StoreUpgrades) StoreLoader { + // sanity checks on store upgrades + if err := checkStoreUpgrade(storeUpgrades); err != nil { + panic(err) + } + return func(store Store) error { latestVersion, err := store.GetLatestVersion() if err != nil { @@ -88,3 +94,38 @@ func UpgradeStoreLoader(upgradeHeight int64, storeUpgrades *store.StoreUpgrades) return DefaultStoreLoader(store) } } + +// checkStoreUpgrade performs sanity checks on the store upgrades +func checkStoreUpgrade(storeUpgrades *store.StoreUpgrades) error { + if storeUpgrades == nil { + return errors.New("store upgrades cannot be nil") + } + + // check for duplicates + exists := make(map[string]bool) + for _, key := range storeUpgrades.Added { + if exists[key] { + return fmt.Errorf("store upgrade has duplicate key %s in added", key) + } + + if storeUpgrades.IsDeleted(key) { + return fmt.Errorf("store upgrade has key %s in both added and deleted", key) + } + + exists[key] = true + } + exists = make(map[string]bool) + for _, key := range storeUpgrades.Deleted { + if exists[key] { + return fmt.Errorf("store upgrade has duplicate key %s in deleted", key) + } + + if storeUpgrades.IsAdded(key) { + return fmt.Errorf("store upgrade has key %s in both added and deleted", key) + } + + exists[key] = true + } + + return nil +} diff --git a/runtime/v2/store_test.go b/runtime/v2/store_test.go new file mode 100644 index 000000000000..64ad7e9cc274 --- /dev/null +++ b/runtime/v2/store_test.go @@ -0,0 +1,71 @@ +package runtime + +import ( + "testing" + + corestore "cosmossdk.io/core/store" +) + +func TestCheckStoreUpgrade(t *testing.T) { + tests := []struct { + name string + storeUpgrades *corestore.StoreUpgrades + wantErr bool + errMsg string + }{ + { + name: "Nil StoreUpgrades", + storeUpgrades: nil, + wantErr: true, + errMsg: "store upgrades cannot be nil", + }, + { + name: "Valid StoreUpgrades", + storeUpgrades: &corestore.StoreUpgrades{ + Added: []string{"store1", "store2"}, + Deleted: []string{"store3", "store4"}, + }, + wantErr: false, + }, + { + name: "Duplicate key in Added", + storeUpgrades: &corestore.StoreUpgrades{ + Added: []string{"store1", "store2", "store1"}, + Deleted: []string{"store3"}, + }, + wantErr: true, + errMsg: "store upgrade has duplicate key store1 in added", + }, + { + name: "Duplicate key in Deleted", + storeUpgrades: &corestore.StoreUpgrades{ + Added: []string{"store1"}, + Deleted: []string{"store2", "store3", "store2"}, + }, + wantErr: true, + errMsg: "store upgrade has duplicate key store2 in deleted", + }, + { + name: "Key in both Added and Deleted", + storeUpgrades: &corestore.StoreUpgrades{ + Added: []string{"store1", "store2"}, + Deleted: []string{"store2", "store3"}, + }, + wantErr: true, + errMsg: "store upgrade has key store2 in both added and deleted", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := checkStoreUpgrade(tt.storeUpgrades) + if (err != nil) != tt.wantErr { + t.Errorf("checkStoreUpgrade() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil && err.Error() != tt.errMsg { + t.Errorf("checkStoreUpgrade() error message = %v, want %v", err.Error(), tt.errMsg) + } + }) + } +}