-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(runtime(v2)): add sanity checks on store upgrades (#21748)
(cherry picked from commit c9f0e2e) # Conflicts: # runtime/v2/store.go
- Loading branch information
1 parent
e9aece0
commit 81ba182
Showing
4 changed files
with
309 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
}) | ||
} | ||
} |