Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[OTE-882] Add prefix to accountplus keeper #2526

Merged
merged 5 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions protocol/app/upgrades/v8.0/upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package v_8_0

import (
"bytes"
"fmt"

storetypes "cosmossdk.io/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
accountpluskeeper "github.com/dydxprotocol/v4-chain/protocol/x/accountplus/keeper"
accountplustypes "github.com/dydxprotocol/v4-chain/protocol/x/accountplus/types"
)

// Migrate accountplus AccountState in kvstore from non-prefixed keys to prefixed keys
func migrateAccountplusAccountState(ctx sdk.Context, k accountpluskeeper.Keeper) {
ctx.Logger().Info("Migrating accountplus module AccountState in kvstore from non-prefixed keys to prefixed keys")

store := ctx.KVStore(k.GetStoreKey())

// Iterate on unprefixed keys
iterator := storetypes.KVStorePrefixIterator(store, nil)
defer iterator.Close()

teddyding marked this conversation as resolved.
Show resolved Hide resolved
for ; iterator.Valid(); iterator.Next() {
key := iterator.Key()

// Double check that key does not have prefix
if bytes.HasPrefix(key, []byte(accountplustypes.AccountStateKeyPrefix)) {
panic(fmt.Sprintf("unexpected key with prefix %X found during migration", accountplustypes.AccountStateKeyPrefix))
}
teddyding marked this conversation as resolved.
Show resolved Hide resolved

value := iterator.Value()
var accountState accountplustypes.AccountState
if err := k.GetCdc().Unmarshal(value, &accountState); err != nil {
panic(fmt.Sprintf("failed to unmarshal AccountState for key %X: %s", key, err))
}
teddyding marked this conversation as resolved.
Show resolved Hide resolved

// SetAccountState stores with prefix
k.SetAccountState(ctx, key, accountState)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this break the contract of iterators?

	// Iterator over a domain of keys in ascending order. End is exclusive.
	// Start must be less than end, or the Iterator is invalid.
	// Iterator must be closed by caller.
	// To iterate over entire domain, use store.Iterator(nil, nil)
	// CONTRACT: No writes may happen within a domain while an iterator exists over it.
	// Exceptionally allowed for cachekv.Store, safe to write in the modules.
	Iterator(start, end []byte) Iterator

e.g. is it safe to modify state while having an open iterator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be safe, I will collect the items and set and remove outside of iteration


// Delete unprefixed key
store.Delete(key)
}

ctx.Logger().Info("Successfully migrated accountplus AccountState keys")
}

// TODO: Scaffolding for upgrade: https://linear.app/dydx/issue/OTE-886/v8-upgrade-handler-scaffold
71 changes: 71 additions & 0 deletions protocol/app/upgrades/v8.0/upgrade_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package v_8_0

import (
"testing"

"cosmossdk.io/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
testapp "github.com/dydxprotocol/v4-chain/protocol/testutil/app"
accountplustypes "github.com/dydxprotocol/v4-chain/protocol/x/accountplus/types"
"github.com/stretchr/testify/suite"
)

type UpgradeTestSuite struct {
suite.Suite

tApp *testapp.TestApp
Ctx sdk.Context
}

func TestUpgradeTestSuite(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my understanding did you write these tests based on some existing template? Have we considered using the conventional upgrade containter test (example), which is more end-to-end?

The current test more like a unit test on migrateAccountplusAccountState - no upgrade is actually being run and we are just calling migrateAccountplusAccountState with whitebox set-up and postcheck. Probably more accurate to rename this test to TestMigrateAccountplusAccountState.

Copy link
Contributor Author

@jerryfan01234 jerryfan01234 Oct 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spoke with Jay on this and we thought unit test should be sufficient for this. I looked at the container e2e tests which is the convention, but I don't think accountplus can be tested in that manner right now because query has not been implemented.

suite.Run(t, new(UpgradeTestSuite))
}

func (s *UpgradeTestSuite) SetupTest() {
s.tApp = testapp.NewTestAppBuilder(s.T()).Build()
s.Ctx = s.tApp.InitChain()
}

func (s *UpgradeTestSuite) TestUpgrade_MigrateAccountplusAccountState() {
ctx := s.Ctx
store := ctx.KVStore(s.tApp.App.AccountPlusKeeper.GetStoreKey())
prefixStore := prefix.NewStore(store, []byte(accountplustypes.AccountStateKeyPrefix))

// Create some AccountState with no prefixes
addresses := []string{"address1", "address2", "address3"}
for _, addr := range addresses {
accAddress := sdk.AccAddress([]byte(addr))
accountState := accountplustypes.AccountState{
Address: addr,
TimestampNonceDetails: accountplustypes.TimestampNonceDetails{
TimestampNonces: []uint64{1, 2, 3},
MaxEjectedNonce: 0,
},
}
bz := s.tApp.App.AccountPlusKeeper.GetCdc().MustMarshal(&accountState)
store.Set(accAddress, bz)
}

// Verify unprefixed keys were successfully created
for _, addr := range addresses {
accAddress := sdk.AccAddress([]byte(addr))
bz := store.Get(accAddress)
s.Require().NotNil(bz, "Unprefixed key not created for %s", addr)
}

// Migrate
migrateAccountplusAccountState(ctx, s.tApp.App.AccountPlusKeeper)

// Verify that unprefixed keys are deleted and prefixed keys exist
for _, addr := range addresses {
accAddress := sdk.AccAddress([]byte(addr))

// Check that the old key no longer exists
bzOld := store.Get(accAddress)
s.Require().Nil(bzOld, "Unprefixed AccountState should be deleted for %s", addr)

// Check that the new prefixed key exists
bzNew := prefixStore.Get(accAddress)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be relatively easy to not only check the node keys but also the content

s.Require().NotNil(bzNew, "Prefixed AccountState should exist for %s", addr)
}
}
jerryfan01234 marked this conversation as resolved.
Show resolved Hide resolved
39 changes: 21 additions & 18 deletions protocol/x/accountplus/keeper/keeper.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package keeper

import (
"bytes"
"errors"
"fmt"
"strings"

"cosmossdk.io/log"
"cosmossdk.io/store/prefix"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -38,6 +37,14 @@ func NewKeeper(
}
}

func (k Keeper) GetStoreKey() storetypes.StoreKey {
return k.storeKey
}

func (k Keeper) GetCdc() codec.BinaryCodec {
return k.cdc
}

func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With(log.ModuleKey, fmt.Sprintf("x/%s", types.ModuleName))
}
Expand All @@ -46,26 +53,22 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger {
func (k Keeper) InitializeForGenesis(ctx sdk.Context) {
}

// Get all account details pairs in store
// Get all AccountStates from kvstore
func (k Keeper) GetAllAccountStates(ctx sdk.Context) ([]types.AccountState, error) {
store := ctx.KVStore(k.storeKey)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify next 3 lines by:

iterator := storetypes.KVStorePrefixIterator(ctx.KVStore(k.storeKey), []byte(types.AccountStateKeyPrefix))

prefixStore := prefix.NewStore(store, []byte(types.AccountStateKeyPrefix))

iterator := storetypes.KVStorePrefixIterator(store, nil)
iterator := prefixStore.Iterator(nil, nil)
defer iterator.Close()

accounts := []types.AccountState{}
for ; iterator.Valid(); iterator.Next() {
key := iterator.Key()

// Temporary workaround to exclude smart account kv pairs.
if bytes.HasPrefix(key, []byte(types.SmartAccountKeyPrefix)) {
continue
var accountState types.AccountState
err := k.cdc.Unmarshal(iterator.Value(), &accountState)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal account state: %w", err)
}

accountState, found := k.GetAccountState(ctx, key)
if !found {
return accounts, errors.New("Could not get account state for address: " + sdk.AccAddress(iterator.Key()).String())
}
accounts = append(accounts, accountState)
}

Expand Down Expand Up @@ -144,7 +147,7 @@ func (k Keeper) GetAllAuthenticatorData(ctx sdk.Context) ([]types.AuthenticatorD
return accountAuthenticators, nil
}

func GetAccountPlusStateWithTimestampNonceDetails(
func AccountStateFromTimestampNonceDetails(
Copy link
Contributor

@teddyding teddyding Oct 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is only used for newly initialized account state

Suggested change
func AccountStateFromTimestampNonceDetails(
func NewAccountStateFromTimestampNonceDetails(

address sdk.AccAddress,
tsNonce uint64,
) types.AccountState {
Expand All @@ -162,8 +165,8 @@ func (k Keeper) GetAccountState(
ctx sdk.Context,
address sdk.AccAddress,
) (types.AccountState, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(address.Bytes())
prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(types.AccountStateKeyPrefix))
bz := prefixStore.Get(address.Bytes())
if bz == nil {
return types.AccountState{}, false
}
Expand All @@ -185,9 +188,9 @@ func (k Keeper) SetAccountState(
address sdk.AccAddress,
accountState types.AccountState,
) {
store := ctx.KVStore(k.storeKey)
prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(types.AccountStateKeyPrefix))
bz := k.cdc.MustMarshal(&accountState)
store.Set(address.Bytes(), bz)
prefixStore.Set(address.Bytes(), bz)
}

func (k Keeper) HasAuthority(authority string) bool {
Expand Down
46 changes: 46 additions & 0 deletions protocol/x/accountplus/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,52 @@ func (s *KeeperTestSuite) SetupTest() {
)
}

func (s *KeeperTestSuite) TestKeeper_Set_Get_GetAllAccountState() {
ctx := s.Ctx
address := "address1"

// SetAccountState
s.tApp.App.AccountPlusKeeper.SetAccountState(
ctx,
sdk.AccAddress([]byte(address)),
types.AccountState{
Address: address,
TimestampNonceDetails: types.TimestampNonceDetails{
TimestampNonces: []uint64{1, 2, 3},
MaxEjectedNonce: 0,
},
},
)

// GetAccountState
_, found := s.tApp.App.AccountPlusKeeper.GetAccountState(ctx, sdk.AccAddress([]byte(address)))
s.Require().True(found, "Account state not found")

// GetAllAccountStates
accountStates, err := s.tApp.App.AccountPlusKeeper.GetAllAccountStates(ctx)
s.Require().NoError(err, "Should not error when getting all account states")
s.Require().Equal(len(accountStates), 1, "Incorrect number of AccountStates retrieved")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and below let's also compare returned object(s)


// Add one more AccountState and check GetAllAccountStates
// SetAccountState
address2 := "address2"
s.tApp.App.AccountPlusKeeper.SetAccountState(
ctx,
sdk.AccAddress([]byte(address2)),
types.AccountState{
Address: address2,
TimestampNonceDetails: types.TimestampNonceDetails{
TimestampNonces: []uint64{1, 2, 3},
MaxEjectedNonce: 0,
},
},
)

accountStates2, err := s.tApp.App.AccountPlusKeeper.GetAllAccountStates(ctx)
s.Require().NoError(err, "Should not error when getting all account states")
s.Require().Equal(len(accountStates2), 2, "Incorrect number of AccountStates retrieved")
}
teddyding marked this conversation as resolved.
Show resolved Hide resolved

func (s *KeeperTestSuite) TestKeeper_AddAuthenticator() {
ctx := s.Ctx

Expand Down
11 changes: 4 additions & 7 deletions protocol/x/accountplus/keeper/timestampnonce.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (k Keeper) ProcessTimestampNonce(ctx sdk.Context, acc sdk.AccountI, tsNonce
accountState, found := k.GetAccountState(ctx, address)
if !found {
// initialize accountplus state with ts nonce details
k.SetAccountState(ctx, address, GetAccountPlusStateWithTimestampNonceDetails(address, tsNonce))
k.SetAccountState(ctx, address, AccountStateFromTimestampNonceDetails(address, tsNonce))
} else {
EjectStaleTimestampNonces(&accountState, blockTs)
tsNonceAccepted := AttemptTimestampNonceUpdate(tsNonce, &accountState)
Expand All @@ -56,9 +56,10 @@ func (k Keeper) ProcessTimestampNonce(ctx sdk.Context, acc sdk.AccountI, tsNonce
// Inplace eject all stale timestamps.
func EjectStaleTimestampNonces(accountState *types.AccountState, referenceTs uint64) {
tsNonceDetails := &accountState.TimestampNonceDetails
oldestAllowedTs := referenceTs - MaxTimeInPastMs
var newTsNonces []uint64
for _, tsNonce := range tsNonceDetails.TimestampNonces {
if tsNonce >= referenceTs-MaxTimeInPastMs {
if tsNonce >= oldestAllowedTs {
newTsNonces = append(newTsNonces, tsNonce)
} else {
if tsNonce > tsNonceDetails.MaxEjectedNonce {
Expand Down Expand Up @@ -115,9 +116,5 @@ func isLargerThanSmallestValue(value uint64, values []uint64) (bool, int) {
}
}

if value > values[minIndex] {
return true, minIndex
} else {
return false, minIndex
}
return value > values[minIndex], minIndex
}
5 changes: 5 additions & 0 deletions protocol/x/accountplus/types/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ const (
StoreKey = ModuleName
)

// Prefix for account state.
const (
AccountStateKeyPrefix = "AS/"
)

// Below key prefixes are for smart account implementation.
const (
// SmartAccountKeyPrefix is the prefix key for all smart account store state.
Expand Down
Loading