diff --git a/runtime/store.go b/runtime/store.go index 1585bc54af713..a38b24ad2f736 100644 --- a/runtime/store.go +++ b/runtime/store.go @@ -2,6 +2,8 @@ package runtime import ( "context" + "errors" + "fmt" "io" dbm "github.com/cosmos/cosmos-db" @@ -186,6 +188,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 @@ -202,3 +209,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 0000000000000..a583a08d0391e --- /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 new file mode 100644 index 0000000000000..de05a27dfd127 --- /dev/null +++ b/runtime/v2/store.go @@ -0,0 +1,131 @@ +package runtime + +import ( + "errors" + "fmt" + + "cosmossdk.io/core/store" + "cosmossdk.io/server/v2/stf" + storev2 "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/proof" +) + +// NewKVStoreService creates a new KVStoreService. +// This wrapper is kept for backwards compatibility. +// When migrating from runtime to runtime/v2, use runtimev2.NewKVStoreService(storeKey.Name()) instead of runtime.NewKVStoreService(storeKey). +func NewKVStoreService(storeKey string) store.KVStoreService { + return stf.NewKVStoreService([]byte(storeKey)) +} + +type Store interface { + // GetLatestVersion returns the latest version that consensus has been made on + GetLatestVersion() (uint64, error) + // StateLatest returns a readonly view over the latest + // committed state of the store. Alongside the version + // associated with it. + StateLatest() (uint64, store.ReaderMap, error) + + // StateAt returns a readonly view over the provided + // version. Must error when the version does not exist. + StateAt(version uint64) (store.ReaderMap, error) + + // SetInitialVersion sets the initial version of the store. + SetInitialVersion(uint64) error + + // WorkingHash writes the provided changeset to the state and returns + // the working hash of the state. + WorkingHash(changeset *store.Changeset) (store.Hash, error) + + // Commit commits the provided changeset and returns the new state root of the state. + Commit(changeset *store.Changeset) (store.Hash, error) + + // Query is a key/value query directly to the underlying database. This skips the appmanager + Query(storeKey []byte, version uint64, key []byte, prove bool) (storev2.QueryResult, error) + + // GetStateStorage returns the SS backend. + GetStateStorage() storev2.VersionedDatabase + + // GetStateCommitment returns the SC backend. + GetStateCommitment() storev2.Committer + + // LoadVersion loads the RootStore to the given version. + LoadVersion(version uint64) error + + // LoadLatestVersion behaves identically to LoadVersion except it loads the + // latest version implicitly. + LoadLatestVersion() error + + // LastCommitID returns the latest commit ID + LastCommitID() (proof.CommitID, error) +} + +// StoreLoader allows for custom loading of the store, this is useful when upgrading the store from a previous version +type StoreLoader func(store Store) error + +// DefaultStoreLoader just calls LoadLatestVersion on the store +func DefaultStoreLoader(store Store) error { + return store.LoadLatestVersion() +} + +// 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 { + return err + } + + if uint64(upgradeHeight) == latestVersion+1 { + if len(storeUpgrades.Deleted) > 0 || len(storeUpgrades.Added) > 0 { + if upgrader, ok := store.(storev2.UpgradeableStore); ok { + return upgrader.LoadVersionAndUpgrade(latestVersion, storeUpgrades) + } + + return fmt.Errorf("store does not support upgrades") + } + } + + 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 0000000000000..64ad7e9cc2744 --- /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) + } + }) + } +}