Skip to content

Commit

Permalink
feat(runtime(v2)): add sanity checks on store upgrades (#21748)
Browse files Browse the repository at this point in the history
(cherry picked from commit c9f0e2e)

# Conflicts:
#	runtime/v2/store.go
  • Loading branch information
julienrbrt authored and mergify[bot] committed Sep 17, 2024
1 parent e9aece0 commit 81ba182
Show file tree
Hide file tree
Showing 4 changed files with 309 additions and 0 deletions.
42 changes: 42 additions & 0 deletions runtime/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package runtime

import (
"context"
"errors"
"fmt"
"io"

dbm "github.com/cosmos/cosmos-db"
Expand Down Expand Up @@ -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
Expand All @@ -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
}
65 changes: 65 additions & 0 deletions runtime/store_test.go
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)
}
})
}
}
131 changes: 131 additions & 0 deletions runtime/v2/store.go
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
}
71 changes: 71 additions & 0 deletions runtime/v2/store_test.go
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)
}
})
}
}

0 comments on commit 81ba182

Please sign in to comment.