refactor(bank): move bank balances to use collections (#15327)

This commit is contained in:
testinginprod 2023-04-11 15:06:04 +02:00 committed by GitHub
parent 3d1a0b8840
commit 7ab0dfc494
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 230 additions and 354 deletions

View File

@ -173,7 +173,7 @@ func TestBaseApp_BlockGas(t *testing.T) {
require.Equal(t, []byte("ok"), okValue)
}
// check block gas is always consumed
baseGas := uint64(51732) // baseGas is the gas consumed before tx msg
baseGas := uint64(50702) // baseGas is the gas consumed before tx msg
expGasConsumed := addUint64Saturating(tc.gasToConsume, baseGas)
if expGasConsumed > txtypes.MaxGasWanted {
// capped by gasLimit

View File

@ -3,6 +3,7 @@ package rpc_test
import (
"context"
"fmt"
"github.com/cosmos/cosmos-sdk/types/address"
"strconv"
"testing"
@ -112,7 +113,7 @@ func (s *IntegrationTestSuite) TestQueryABCIHeight() {
req := abci.RequestQuery{
Path: fmt.Sprintf("store/%s/key", banktypes.StoreKey),
Height: tc.reqHeight,
Data: banktypes.CreateAccountBalancesPrefix(val.Address),
Data: address.MustLengthPrefix(val.Address),
Prove: true,
}

View File

@ -1,15 +0,0 @@
//go:build gofuzz || go1.18
package tests
import (
"testing"
"github.com/cosmos/cosmos-sdk/x/bank/types"
)
func FuzzXBankTypesAddressFromBalancesStore(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
types.AddressAndDenomFromBalancesStore(data)
})
}

2
go.mod
View File

@ -4,7 +4,7 @@ module github.com/cosmos/cosmos-sdk
require (
cosmossdk.io/api v0.4.0
cosmossdk.io/collections v0.0.0-20230309163709-87da587416ba
cosmossdk.io/collections v0.0.0-20230411101845-3d1a0b8840e4
cosmossdk.io/core v0.6.1
cosmossdk.io/depinject v1.0.0-alpha.3
cosmossdk.io/errors v1.0.0-beta.7

4
go.sum
View File

@ -37,8 +37,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
cosmossdk.io/api v0.4.0 h1:x90DmdidP6EhzktAa/6/IofSHidDnPjahdlrUvyQZQw=
cosmossdk.io/api v0.4.0/go.mod h1:TWDzBhUBhI1LhSf2XSYpfIBf6D4mbLu/fvzvDfhcaYM=
cosmossdk.io/collections v0.0.0-20230309163709-87da587416ba h1:S4PYij/tX3Op/hwenVEN9D+M27JRcwSwVqE3UA0BnwM=
cosmossdk.io/collections v0.0.0-20230309163709-87da587416ba/go.mod h1:lpS+G8bGC2anqzWdndTzjnQnuMO/qAcgZUkGJp4i3rc=
cosmossdk.io/collections v0.0.0-20230411101845-3d1a0b8840e4 h1:QQZ0Qz8Gy/EmUNMRiHkUPG3BMA6OqEBp67IsfKETXIU=
cosmossdk.io/collections v0.0.0-20230411101845-3d1a0b8840e4/go.mod h1:/vS4ugR7ad3IciUd5TQuP2Ldz3NukHK2u/l5xTxXbbE=
cosmossdk.io/core v0.6.1 h1:OBy7TI2W+/gyn2z40vVvruK3di+cAluinA6cybFbE7s=
cosmossdk.io/core v0.6.1/go.mod h1:g3MMBCBXtxbDWBURDVnJE7XML4BG5qENhs0gzkcpuFA=
cosmossdk.io/depinject v1.0.0-alpha.3 h1:6evFIgj//Y3w09bqOUOzEpFj5tsxBqdc5CfkO7z+zfw=

View File

@ -36,7 +36,7 @@ require (
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.13.0 // indirect
cloud.google.com/go/storage v1.30.0 // indirect
cosmossdk.io/collections v0.0.0-20230309163709-87da587416ba // indirect
cosmossdk.io/collections v0.0.0-20230411101845-3d1a0b8840e4 // indirect
cosmossdk.io/errors v1.0.0-beta.7 // indirect
cosmossdk.io/x/tx v0.5.1-0.20230407182919-057d2e09bd63 // indirect
filippo.io/edwards25519 v1.0.0 // indirect

View File

@ -190,8 +190,8 @@ cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1V
cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
cosmossdk.io/client/v2 v2.0.0-20230309163709-87da587416ba h1:LuPHCncU2KLMNPItFECs709uo46I9wSu2fAWYVCx+/U=
cosmossdk.io/client/v2 v2.0.0-20230309163709-87da587416ba/go.mod h1:SXdwqO7cN5htalh/lhXWP8V4zKtBrhhcSTU+ytuEtmM=
cosmossdk.io/collections v0.0.0-20230309163709-87da587416ba h1:S4PYij/tX3Op/hwenVEN9D+M27JRcwSwVqE3UA0BnwM=
cosmossdk.io/collections v0.0.0-20230309163709-87da587416ba/go.mod h1:lpS+G8bGC2anqzWdndTzjnQnuMO/qAcgZUkGJp4i3rc=
cosmossdk.io/collections v0.0.0-20230411101845-3d1a0b8840e4 h1:QQZ0Qz8Gy/EmUNMRiHkUPG3BMA6OqEBp67IsfKETXIU=
cosmossdk.io/collections v0.0.0-20230411101845-3d1a0b8840e4/go.mod h1:/vS4ugR7ad3IciUd5TQuP2Ldz3NukHK2u/l5xTxXbbE=
cosmossdk.io/core v0.6.1 h1:OBy7TI2W+/gyn2z40vVvruK3di+cAluinA6cybFbE7s=
cosmossdk.io/core v0.6.1/go.mod h1:g3MMBCBXtxbDWBURDVnJE7XML4BG5qENhs0gzkcpuFA=
cosmossdk.io/depinject v1.0.0-alpha.3 h1:6evFIgj//Y3w09bqOUOzEpFj5tsxBqdc5CfkO7z+zfw=

View File

@ -37,7 +37,7 @@ require (
cloud.google.com/go/iam v0.13.0 // indirect
cloud.google.com/go/storage v1.30.0 // indirect
cosmossdk.io/client/v2 v2.0.0-20230309163709-87da587416ba // indirect
cosmossdk.io/collections v0.0.0-20230309163709-87da587416ba // indirect
cosmossdk.io/collections v0.0.0-20230411101845-3d1a0b8840e4 // indirect
cosmossdk.io/core v0.6.1 // indirect
filippo.io/edwards25519 v1.0.0 // indirect
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect

View File

@ -190,8 +190,8 @@ cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1V
cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
cosmossdk.io/client/v2 v2.0.0-20230309163709-87da587416ba h1:LuPHCncU2KLMNPItFECs709uo46I9wSu2fAWYVCx+/U=
cosmossdk.io/client/v2 v2.0.0-20230309163709-87da587416ba/go.mod h1:SXdwqO7cN5htalh/lhXWP8V4zKtBrhhcSTU+ytuEtmM=
cosmossdk.io/collections v0.0.0-20230309163709-87da587416ba h1:S4PYij/tX3Op/hwenVEN9D+M27JRcwSwVqE3UA0BnwM=
cosmossdk.io/collections v0.0.0-20230309163709-87da587416ba/go.mod h1:lpS+G8bGC2anqzWdndTzjnQnuMO/qAcgZUkGJp4i3rc=
cosmossdk.io/collections v0.0.0-20230411101845-3d1a0b8840e4 h1:QQZ0Qz8Gy/EmUNMRiHkUPG3BMA6OqEBp67IsfKETXIU=
cosmossdk.io/collections v0.0.0-20230411101845-3d1a0b8840e4/go.mod h1:/vS4ugR7ad3IciUd5TQuP2Ldz3NukHK2u/l5xTxXbbE=
cosmossdk.io/core v0.6.1 h1:OBy7TI2W+/gyn2z40vVvruK3di+cAluinA6cybFbE7s=
cosmossdk.io/core v0.6.1/go.mod h1:g3MMBCBXtxbDWBURDVnJE7XML4BG5qENhs0gzkcpuFA=
cosmossdk.io/depinject v1.0.0-alpha.3 h1:6evFIgj//Y3w09bqOUOzEpFj5tsxBqdc5CfkO7z+zfw=

View File

@ -478,5 +478,5 @@ func TestGRPCDenomOwners(t *testing.T) {
req := &banktypes.QueryDenomOwnersRequest{
Denom: coin1.GetDenom(),
}
testdata.DeterministicIterations(f.ctx, t, req, f.queryClient.DenomOwners, 2525, false)
testdata.DeterministicIterations(f.ctx, t, req, f.queryClient.DenomOwners, 2516, false)
}

View File

@ -37,7 +37,7 @@ type TestContext struct {
CMS store.CommitMultiStore
}
func DefaultContextWithDB(t *testing.T, key, tkey storetypes.StoreKey) TestContext {
func DefaultContextWithDB(t testing.TB, key, tkey storetypes.StoreKey) TestContext {
db := dbm.NewMemDB()
cms := store.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics())
cms.MountStoreWithDB(key, storetypes.StoreTypeIAVL, db)

View File

@ -85,6 +85,40 @@ func (a genericAddressKey[T]) SizeNonTerminal(key T) int {
return collections.BytesKey.SizeNonTerminal(key)
}
// Deprecated: genericAddressIndexKey is a special key codec used to retain state backwards compatibility
// when a generic address key (be: AccAddress, ValAddress, ConsAddress), is used as an index key.
// More docs can be found in the AddressKeyAsIndexKey function.
type genericAddressIndexKey[T addressUnion] struct {
collcodec.KeyCodec[T]
}
func (g genericAddressIndexKey[T]) Encode(buffer []byte, key T) (int, error) {
return g.EncodeNonTerminal(buffer, key)
}
func (g genericAddressIndexKey[T]) Decode(buffer []byte) (int, T, error) {
return g.DecodeNonTerminal(buffer)
}
func (g genericAddressIndexKey[T]) Size(key T) int { return g.SizeNonTerminal(key) }
func (g genericAddressIndexKey[T]) KeyType() string { return "index_key/" + g.KeyCodec.KeyType() }
// Deprecated: AddressKeyAsIndexKey implements an SDK backwards compatible indexing key encoder
// for addresses.
// The status quo in the SDK is that address keys are length prefixed even when they're the
// last part of a composite key. This should never be used unless to retain state compatibility.
// For example, a composite key composed of `[string, address]` in theory would need you only to
// define a way to understand when the string part finishes, we usually do this by appending a null
// byte to the string, then when you know when the string part finishes, it's logical that the
// part which remains is the address key. In the SDK instead we prepend to the address key its
// length too.
func AddressKeyAsIndexKey[T addressUnion](keyCodec collcodec.KeyCodec[T]) collcodec.KeyCodec[T] {
return genericAddressIndexKey[T]{
keyCodec,
}
}
// Collection Codecs
type intValueCodec struct{}

View File

@ -18,4 +18,8 @@ func TestCollectionsCorrectness(t *testing.T) {
t.Run("ConsAddress", func(t *testing.T) {
colltest.TestKeyCodec(t, ConsAddressKey, ConsAddress{0x32, 0x0, 0x0, 0x3})
})
t.Run("AddressIndexingKey", func(t *testing.T) {
colltest.TestKeyCodec(t, AddressKeyAsIndexKey(AccAddressKey), AccAddress{0x2, 0x5, 0x8})
})
}

View File

@ -2,14 +2,22 @@ package query
import (
"context"
"errors"
"fmt"
"cosmossdk.io/collections"
collcodec "cosmossdk.io/collections/codec"
storetypes "cosmossdk.io/store/types"
"errors"
"fmt"
)
// WithCollectionPaginationPairPrefix applies a prefix to a collection, whose key is a collection.Pair,
// being paginated that needs prefixing.
func WithCollectionPaginationPairPrefix[K1, K2 any](prefix K1) func(o *CollectionsPaginateOptions[collections.Pair[K1, K2]]) {
return func(o *CollectionsPaginateOptions[collections.Pair[K1, K2]]) {
prefix := collections.PairPrefix[K1, K2](prefix)
o.Prefix = &prefix
}
}
// CollectionsPaginateOptions provides extra options for pagination in collections.
type CollectionsPaginateOptions[K any] struct {
// Prefix allows to optionally set a prefix for the pagination.
@ -41,7 +49,7 @@ func CollectionFilteredPaginate[K, V any, C Collection[K, V]](
ctx context.Context,
coll C,
pageReq *PageRequest,
predicateFunc func(key K, value V) (include bool),
predicateFunc func(key K, value V) (include bool, err error),
opts ...func(opt *CollectionsPaginateOptions[K]),
) ([]collections.KeyValue[K, V], *PageResponse, error) {
if pageReq == nil {
@ -89,7 +97,7 @@ func CollectionFilteredPaginate[K, V any, C Collection[K, V]](
}
// invalid iter error is ignored to retain Paginate behavior
if errors.Is(err, collections.ErrInvalidIterator) {
return results, pageRes, nil
return results, new(PageResponse), nil
}
// strip the prefix from next key
if len(pageRes.NextKey) != 0 && prefix != nil {
@ -108,7 +116,7 @@ func collFilteredPaginateNoKey[K, V any, C Collection[K, V]](
offset uint64,
limit uint64,
countTotal bool,
predicateFunc func(K, V) bool,
predicateFunc func(K, V) (bool, error),
) ([]collections.KeyValue[K, V], *PageResponse, error) {
iterator, err := getCollIter[K, V](ctx, coll, prefix, nil, reverse)
if err != nil {
@ -137,12 +145,17 @@ func collFilteredPaginateNoKey[K, V any, C Collection[K, V]](
// if no predicate function is specified then we just include the result
if predicateFunc == nil {
results = append(results, kv)
count++
// if predicate function is defined we check if the result matches the filtering criteria
} else if predicateFunc(kv.Key, kv.Value) {
results = append(results, kv)
count++
} else {
include, err := predicateFunc(kv.Key, kv.Value)
if err != nil {
return nil, nil, err
}
if include {
results = append(results, kv)
}
}
count++
// second case, we found all the objects specified within the limit
case count == limit:
key, err := iterator.Key()
@ -200,7 +213,7 @@ func collFilteredPaginateByKey[K, V any, C Collection[K, V]](
key []byte,
reverse bool,
limit uint64,
predicateFunc func(K, V) bool,
predicateFunc func(K, V) (bool, error),
) ([]collections.KeyValue[K, V], *PageResponse, error) {
iterator, err := getCollIter[K, V](ctx, coll, prefix, key, reverse)
if err != nil {
@ -237,13 +250,18 @@ func collFilteredPaginateByKey[K, V any, C Collection[K, V]](
// if no predicate is specified then we just append the result
if predicateFunc == nil {
results = append(results, kv)
count++
// if predicate is applied we execute the predicate function
// and append only if predicateFunc yields true.
} else if predicateFunc(kv.Key, kv.Value) {
results = append(results, kv)
count++
} else {
include, err := predicateFunc(kv.Key, kv.Value)
if err != nil {
return nil, nil, err
}
if include {
results = append(results, kv)
}
}
count++
}
return results, &PageResponse{
@ -258,14 +276,20 @@ func encodeCollKey[K, V any, C Collection[K, V]](coll C, key K) ([]byte, error)
return buffer, err
}
func getCollIter[K, V any, C Collection[K, V]](ctx context.Context, coll C, prefix, start []byte, reverse bool) (collections.Iterator[K, V], error) {
func getCollIter[K, V any, C Collection[K, V]](ctx context.Context, coll C, prefix []byte, start []byte, reverse bool) (collections.Iterator[K, V], error) {
// TODO: maybe can be simplified
if reverse {
var end []byte
if prefix != nil {
start = storetypes.PrefixEndBytes(append(prefix, start...))
end = prefix
}
return coll.IterateRaw(ctx, end, start, collections.OrderDescending)
}
var end []byte
if prefix != nil {
start = append(prefix, start...)
end = storetypes.PrefixEndBytes(prefix)
}
if reverse {
return coll.IterateRaw(ctx, nil, start, collections.OrderDescending)
}
return coll.IterateRaw(ctx, start, end, collections.OrderAscending)
}

View File

@ -48,7 +48,7 @@ func TestCollectionPagination(t *testing.T) {
type test struct {
req *PageRequest
expResp *PageResponse
filter func(key, value uint64) bool
filter func(key, value uint64) (bool, error)
expResults []collections.KeyValue[uint64, uint64]
wantErr error
}
@ -99,15 +99,14 @@ func TestCollectionPagination(t *testing.T) {
Limit: 3,
},
expResp: &PageResponse{
NextKey: encodeKey(5),
NextKey: encodeKey(3),
},
filter: func(key, value uint64) bool {
return key%2 == 0
filter: func(key, value uint64) (bool, error) {
return key%2 == 0, nil
},
expResults: []collections.KeyValue[uint64, uint64]{
{Key: 0, Value: 0},
{Key: 2, Value: 2},
{Key: 4, Value: 4},
},
},
"filtered with key": {
@ -116,15 +115,14 @@ func TestCollectionPagination(t *testing.T) {
Limit: 3,
},
expResp: &PageResponse{
NextKey: encodeKey(7),
NextKey: encodeKey(5),
},
filter: func(key, value uint64) bool {
return key%2 == 0
filter: func(key, value uint64) (bool, error) {
return key%2 == 0, nil
},
expResults: []collections.KeyValue[uint64, uint64]{
{Key: 2, Value: 2},
{Key: 4, Value: 4},
{Key: 6, Value: 6},
},
},
}

View File

@ -231,7 +231,7 @@ func (s *paginationTestSuite) TestReversePagination() {
request := types.NewQueryAllBalancesRequest(addr1, pageReq, false)
res1, err := queryClient.AllBalances(gocontext.Background(), request)
s.Require().NoError(err)
s.Require().Equal(res1.Balances.Len(), 2)
s.Require().Equal(2, res1.Balances.Len())
s.Require().NotNil(res1.Pagination.NextKey)
s.T().Log("verify paginate with custom limit and countTotal, Reverse false")

View File

@ -1,6 +1,7 @@
package keeper
import (
"cosmossdk.io/collections"
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
@ -24,8 +25,11 @@ func (k BaseKeeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) {
for _, balance := range genState.Balances {
addr := balance.GetAddress()
if err := k.initBalances(ctx, addr, balance.Coins); err != nil {
panic(fmt.Errorf("error on setting balances %w", err))
for _, coin := range balance.Coins {
err := k.Balances.Set(ctx, collections.Join(addr, coin.Denom), coin.Amount)
if err != nil {
panic(err)
}
}
totalSupply = totalSupply.Add(balance.Coins...)

View File

@ -2,7 +2,7 @@ package keeper
import (
"context"
"cosmossdk.io/collections"
"cosmossdk.io/math"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@ -51,26 +51,17 @@ func (k BaseKeeper) AllBalances(ctx context.Context, req *types.QueryAllBalances
sdkCtx := sdk.UnwrapSDKContext(ctx)
balances := sdk.NewCoins()
accountStore := k.getAccountStore(sdkCtx, addr)
pageRes, err := query.Paginate(accountStore, req.Pagination, func(key, value []byte) error {
denom := string(key)
// IBC denom metadata will be registered in ibc-go after first mint
//
// Since: ibc-go v7
_, pageRes, err := query.CollectionFilteredPaginate(ctx, k.Balances, req.Pagination, func(key collections.Pair[sdk.AccAddress, string], value math.Int) (include bool, err error) {
denom := key.K2()
if req.ResolveDenom {
if metadata, ok := k.GetDenomMetaData(sdkCtx, denom); ok {
denom = metadata.Display
}
}
balance, err := UnmarshalBalanceCompat(k.cdc, value, denom)
if err != nil {
return err
}
balances = append(balances, balance)
return nil
})
balances = append(balances, sdk.NewCoin(denom, value))
return false, nil // we don't include results because we're appending them here.
}, query.WithCollectionPaginationPairPrefix[sdk.AccAddress, string](addr))
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "paginate: %v", err)
}
@ -93,13 +84,13 @@ func (k BaseKeeper) SpendableBalances(ctx context.Context, req *types.QuerySpend
sdkCtx := sdk.UnwrapSDKContext(ctx)
balances := sdk.NewCoins()
accountStore := k.getAccountStore(sdkCtx, addr)
zeroAmt := math.ZeroInt()
pageRes, err := query.Paginate(accountStore, req.Pagination, func(key, _ []byte) error {
balances = append(balances, sdk.NewCoin(string(key), zeroAmt))
return nil
})
_, pageRes, err := query.CollectionFilteredPaginate(ctx, k.Balances, req.Pagination, func(key collections.Pair[sdk.AccAddress, string], _ math.Int) (include bool, err error) {
balances = append(balances, sdk.NewCoin(key.K2(), zeroAmt))
return false, nil // not including results as they're appended here
}, query.WithCollectionPaginationPairPrefix[sdk.AccAddress, string](addr))
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "paginate: %v", err)
}
@ -237,34 +228,24 @@ func (k BaseKeeper) DenomOwners(
return nil, status.Error(codes.InvalidArgument, err.Error())
}
ctx := sdk.UnwrapSDKContext(goCtx)
denomPrefixStore := k.getDenomAddressPrefixStore(ctx, req.Denom)
var denomOwners []*types.DenomOwner
pageRes, err := query.FilteredPaginate(
denomPrefixStore,
req.Pagination,
func(key, _ []byte, accumulate bool) (bool, error) {
if accumulate {
address, _, err := types.AddressAndDenomFromBalancesStore(key)
if err != nil {
return false, err
}
denomOwners = append(
denomOwners,
&types.DenomOwner{
Address: address.String(),
Balance: k.GetBalance(ctx, address, req.Denom),
},
)
_, pageRes, err := query.CollectionFilteredPaginate(goCtx, k.Balances.Indexes.Denom, req.Pagination,
func(key collections.Pair[string, sdk.AccAddress], value collections.NoValue) (include bool, err error) {
amt, err := k.Balances.Get(goCtx, collections.Join(key.K2(), req.Denom))
if err != nil {
return false, err
}
return true, nil
denomOwners = append(denomOwners, &types.DenomOwner{
Address: key.K2().String(),
Balance: sdk.NewCoin(req.Denom, amt),
})
return false, nil
},
query.WithCollectionPaginationPairPrefix[string, sdk.AccAddress](req.Denom),
)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
return nil, err
}
return &types.QueryDenomOwnersResponse{DenomOwners: denomOwners, Pagination: pageRes}, nil

View File

@ -461,7 +461,7 @@ func (suite *KeeperTestSuite) TestGRPCDenomOwners() {
suite.Require().NoError(keeper.MintCoins(ctx, minttypes.ModuleName, initCoins))
for i := 0; i < 10; i++ {
addr := sdk.AccAddress([]byte(fmt.Sprintf("account-%d", i)))
addr := sdk.AccAddress(fmt.Sprintf("account-%d", i))
bal := sdk.NewCoins(sdk.NewCoin(
sdk.DefaultBondDenom,

View File

@ -1,16 +1,15 @@
package keeper
import (
"cosmossdk.io/collections"
"fmt"
errorsmod "cosmossdk.io/errors"
"cosmossdk.io/store/prefix"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/telemetry"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/address"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/bank/types"
)
@ -277,74 +276,21 @@ func (k BaseSendKeeper) addCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.C
return nil
}
// initBalances sets the balance (multiple coins) for an account by address.
// An error is returned upon failure.
func (k BaseSendKeeper) initBalances(ctx sdk.Context, addr sdk.AccAddress, balances sdk.Coins) error {
accountStore := k.getAccountStore(ctx, addr)
denomPrefixStores := make(map[string]prefix.Store) // memoize prefix stores
for i := range balances {
balance := balances[i]
if !balance.IsValid() {
return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, balance.String())
}
// x/bank invariants prohibit persistence of zero balances
if !balance.IsZero() {
amount, err := balance.Amount.Marshal()
if err != nil {
return err
}
accountStore.Set([]byte(balance.Denom), amount)
denomPrefixStore, ok := denomPrefixStores[balance.Denom]
if !ok {
denomPrefixStore = k.getDenomAddressPrefixStore(ctx, balance.Denom)
denomPrefixStores[balance.Denom] = denomPrefixStore
}
// Store a reverse index from denomination to account address with a
// sentinel value.
denomAddrKey := address.MustLengthPrefix(addr)
if !denomPrefixStore.Has(denomAddrKey) {
denomPrefixStore.Set(denomAddrKey, []byte{0})
}
}
}
return nil
}
// setBalance sets the coin balance for an account by address.
func (k BaseSendKeeper) setBalance(ctx sdk.Context, addr sdk.AccAddress, balance sdk.Coin) error {
if !balance.IsValid() {
return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, balance.String())
}
accountStore := k.getAccountStore(ctx, addr)
denomPrefixStore := k.getDenomAddressPrefixStore(ctx, balance.Denom)
// x/bank invariants prohibit persistence of zero balances
if balance.IsZero() {
accountStore.Delete([]byte(balance.Denom))
denomPrefixStore.Delete(address.MustLengthPrefix(addr))
} else {
amount, err := balance.Amount.Marshal()
err := k.Balances.Remove(ctx, collections.Join(addr, balance.Denom))
if err != nil {
return err
}
accountStore.Set([]byte(balance.Denom), amount)
// Store a reverse index from denomination to account address with a
// sentinel value.
denomAddrKey := address.MustLengthPrefix(addr)
if !denomPrefixStore.Has(denomAddrKey) {
denomPrefixStore.Set(denomAddrKey, []byte{0})
}
return nil
}
return nil
return k.Balances.Set(ctx, collections.Join(addr, balance.Denom), balance.Amount)
}
// IsSendEnabledCoins checks the coins provided and returns an ErrSendDisabled

View File

@ -1,7 +1,9 @@
package keeper
import (
"cosmossdk.io/collections/indexes"
"fmt"
"github.com/cockroachdb/errors"
"cosmossdk.io/collections"
@ -11,7 +13,6 @@ import (
"cosmossdk.io/math"
errorsmod "cosmossdk.io/errors"
"cosmossdk.io/store/prefix"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
@ -39,6 +40,23 @@ type ViewKeeper interface {
IterateAllBalances(ctx sdk.Context, cb func(address sdk.AccAddress, coin sdk.Coin) (stop bool))
}
func newBalancesIndexes(sb *collections.SchemaBuilder) BalancesIndexes {
return BalancesIndexes{
Denom: indexes.NewReversePair[math.Int](
sb, types.DenomAddressPrefix, "address_by_denom_index",
collections.PairKeyCodec(sdk.AddressKeyAsIndexKey(sdk.AccAddressKey), collections.StringKey), // NOTE: refer to the AddressKeyAsIndexKey docs to understand why we do this.
),
}
}
type BalancesIndexes struct {
Denom *indexes.ReversePair[sdk.AccAddress, string, math.Int]
}
func (b BalancesIndexes) IndexesList() []collections.Index[collections.Pair[sdk.AccAddress, string], math.Int] {
return []collections.Index[collections.Pair[sdk.AccAddress, string], math.Int]{b.Denom}
}
// BaseViewKeeper implements a read only keeper implementation of ViewKeeper.
type BaseViewKeeper struct {
cdc codec.BinaryCodec
@ -49,6 +67,7 @@ type BaseViewKeeper struct {
Supply collections.Map[string, math.Int]
DenomMetadata collections.Map[string, types.Metadata]
SendEnabled collections.Map[string, bool]
Balances *collections.IndexedMap[collections.Pair[sdk.AccAddress, string], math.Int, BalancesIndexes]
Params collections.Item[types.Params]
}
@ -62,6 +81,7 @@ func NewBaseViewKeeper(cdc codec.BinaryCodec, storeKey storetypes.StoreKey, ak t
Supply: collections.NewMap(sb, types.SupplyKey, "supply", collections.StringKey, sdk.IntValue),
DenomMetadata: collections.NewMap(sb, types.DenomMetadataPrefix, "denom_metadata", collections.StringKey, codec.CollValue[types.Metadata](cdc)),
SendEnabled: collections.NewMap(sb, types.SendEnabledPrefix, "send_enabled", collections.StringKey, codec.BoolValue), // NOTE: we use a bool value which uses protobuf to retain state backwards compat
Balances: collections.NewIndexedMap(sb, types.BalancesPrefix, "balances", collections.PairKeyCodec(sdk.AccAddressKey, collections.StringKey), types.NewBalanceCompatValueCodec(), newBalancesIndexes(sb)),
Params: collections.NewItem(sb, types.ParamsKey, "params", codec.CollValue[types.Params](cdc)),
}
@ -123,35 +143,22 @@ func (k BaseViewKeeper) GetAccountsBalances(ctx sdk.Context) []types.Balance {
// GetBalance returns the balance of a specific denomination for a given account
// by address.
func (k BaseViewKeeper) GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin {
accountStore := k.getAccountStore(ctx, addr)
bz := accountStore.Get([]byte(denom))
balance, err := UnmarshalBalanceCompat(k.cdc, bz, denom)
amt, err := k.Balances.Get(ctx, collections.Join(addr, denom))
if err != nil {
panic(err)
return sdk.NewCoin(denom, sdk.ZeroInt())
}
return balance
return sdk.NewCoin(denom, amt)
}
// IterateAccountBalances iterates over the balances of a single account and
// provides the token balance to a callback. If true is returned from the
// callback, iteration is halted.
func (k BaseViewKeeper) IterateAccountBalances(ctx sdk.Context, addr sdk.AccAddress, cb func(sdk.Coin) bool) {
accountStore := k.getAccountStore(ctx, addr)
iterator := accountStore.Iterator(nil, nil)
defer sdk.LogDeferred(ctx.Logger(), func() error { return iterator.Close() })
for ; iterator.Valid(); iterator.Next() {
denom := string(iterator.Key())
balance, err := UnmarshalBalanceCompat(k.cdc, iterator.Value(), denom)
if err != nil {
panic(err)
}
if cb(balance) {
break
}
err := k.Balances.Walk(ctx, collections.NewPrefixedPairRange[sdk.AccAddress, string](addr), func(key collections.Pair[sdk.AccAddress, string], value math.Int) bool {
return cb(sdk.NewCoin(key.K2(), value))
})
if err != nil && !errors.Is(err, collections.ErrInvalidIterator) { // TODO(tip): is this the correct strategy
panic(err)
}
}
@ -159,29 +166,11 @@ func (k BaseViewKeeper) IterateAccountBalances(ctx sdk.Context, addr sdk.AccAddr
// denominations that are provided to a callback. If true is returned from the
// callback, iteration is halted.
func (k BaseViewKeeper) IterateAllBalances(ctx sdk.Context, cb func(sdk.AccAddress, sdk.Coin) bool) {
store := ctx.KVStore(k.storeKey)
balancesStore := prefix.NewStore(store, types.BalancesPrefix)
iterator := balancesStore.Iterator(nil, nil)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
address, denom, err := types.AddressAndDenomFromBalancesStore(iterator.Key())
if err != nil {
k.Logger(ctx).With("key", iterator.Key(), "err", err).Error("failed to get address from balances store")
// TODO: revisit, for now, panic here to keep same behavior as in 0.42
// ref: https://github.com/cosmos/cosmos-sdk/issues/7409
panic(err)
}
balance, err := UnmarshalBalanceCompat(k.cdc, iterator.Value(), denom)
if err != nil {
panic(err)
}
if cb(address, balance) {
break
}
err := k.Balances.Walk(ctx, nil, func(key collections.Pair[sdk.AccAddress, string], value math.Int) bool {
return cb(key.K1(), sdk.NewCoin(key.K2(), value))
})
if err != nil {
panic(err)
}
}
@ -261,39 +250,3 @@ func (k BaseViewKeeper) ValidateBalance(ctx sdk.Context, addr sdk.AccAddress) er
return nil
}
// getAccountStore gets the account store of the given address.
func (k BaseViewKeeper) getAccountStore(ctx sdk.Context, addr sdk.AccAddress) prefix.Store {
store := ctx.KVStore(k.storeKey)
return prefix.NewStore(store, types.CreateAccountBalancesPrefix(addr))
}
// getDenomAddressPrefixStore returns a prefix store that acts as a reverse index
// between a denomination and account balance for that denomination.
func (k BaseViewKeeper) getDenomAddressPrefixStore(ctx sdk.Context, denom string) prefix.Store {
return prefix.NewStore(ctx.KVStore(k.storeKey), types.CreateDenomAddressPrefix(denom))
}
// UnmarshalBalanceCompat unmarshal balance amount from storage, it's backward-compatible with the legacy format.
func UnmarshalBalanceCompat(cdc codec.BinaryCodec, bz []byte, denom string) (sdk.Coin, error) {
if err := sdk.ValidateDenom(denom); err != nil {
return sdk.Coin{}, err
}
amount := math.ZeroInt()
if bz == nil {
return sdk.NewCoin(denom, amount), nil
}
if err := amount.Unmarshal(bz); err != nil {
// try to unmarshal with the legacy format.
var balance sdk.Coin
if cdc.Unmarshal(bz, &balance) != nil {
// return with the original error
return sdk.Coin{}, err
}
return balance, nil
}
return sdk.NewCoin(denom, amount), nil
}

View File

@ -65,7 +65,7 @@ func migrateBalanceKeys(store storetypes.KVStore, logger log.Logger) {
for ; oldStoreIter.Valid(); oldStoreIter.Next() {
addr := v1.AddressFromBalancesStore(oldStoreIter.Key())
denom := oldStoreIter.Key()[v1auth.AddrLen:]
newStoreKey := types.CreatePrefixedAccountStoreKey(addr, denom)
newStoreKey := CreatePrefixedAccountStoreKey(addr, denom)
// Set new key on store. Values don't change.
store.Set(newStoreKey, oldStoreIter.Value())
@ -133,3 +133,9 @@ func pruneZeroSupply(store storetypes.KVStore) error {
return nil
}
// CreatePrefixedAccountStoreKey returns the key for the given account and denomination.
// This method can be used when performing an ABCI query for the balance of an account.
func CreatePrefixedAccountStoreKey(addr []byte, denom []byte) []byte {
return append(CreateAccountBalancesPrefix(addr), denom...)
}

View File

@ -94,13 +94,13 @@ func TestBalanceKeysMigration(t *testing.T) {
err = v2bank.MigrateStore(ctx, bankKey, encCfg.Codec)
require.NoError(t, err)
newKey := types.CreatePrefixedAccountStoreKey(addr, []byte(fooCoin.Denom))
newKey := v2bank.CreatePrefixedAccountStoreKey(addr, []byte(fooCoin.Denom))
// -7 because we replaced "balances" with 0x02,
// +1 because we added length-prefix to address.
require.Equal(t, len(oldFooKey)-7+1, len(newKey))
require.Nil(t, store.Get(oldFooKey))
require.Equal(t, fooBz, store.Get(newKey))
newKeyFooBar := types.CreatePrefixedAccountStoreKey(addr, []byte(fooBarCoin.Denom))
newKeyFooBar := v2bank.CreatePrefixedAccountStoreKey(addr, []byte(fooBarCoin.Denom))
require.Nil(t, store.Get(newKeyFooBar)) // after migration zero balances pruned from store.
}

View File

@ -57,7 +57,7 @@ func addDenomReverseIndex(store storetypes.KVStore, cdc codec.BinaryCodec, logge
return err
}
newStore := prefix.NewStore(store, types.CreateAccountBalancesPrefix(addr))
newStore := prefix.NewStore(store, CreateAccountBalancesPrefix(addr))
newStore.Set([]byte(coin.Denom), bz)
denomPrefixStore, ok := denomPrefixStores[balance.Denom]
@ -94,3 +94,8 @@ func migrateDenomMetadata(store storetypes.KVStore, logger log.Logger) error {
return nil
}
// CreateAccountBalancesPrefix creates the prefix for an account's balances.
func CreateAccountBalancesPrefix(addr []byte) []byte {
return append(types.BalancesPrefix.Bytes(), address.MustLengthPrefix(addr)...)
}

View File

@ -42,7 +42,7 @@ func TestMigrateStore(t *testing.T) {
require.NoError(t, v3.MigrateStore(ctx, bankKey, encCfg.Codec))
for _, b := range balances {
addrPrefixStore := prefix.NewStore(store, types.CreateAccountBalancesPrefix(addr))
addrPrefixStore := prefix.NewStore(store, v3.CreateAccountBalancesPrefix(addr))
bz := addrPrefixStore.Get([]byte(b.Denom))
var expected math.Int
require.NoError(t, expected.Unmarshal(bz))

View File

@ -2,9 +2,9 @@ package types
import (
"cosmossdk.io/collections"
collcodec "cosmossdk.io/collections/codec"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/address"
"github.com/cosmos/cosmos-sdk/types/kv"
)
const (
@ -22,12 +22,10 @@ const (
var (
SupplyKey = collections.NewPrefix(0)
DenomMetadataPrefix = collections.NewPrefix(1)
DenomAddressPrefix = []byte{0x03}
// BalancesPrefix is the prefix for the account balances store. We use a byte
// (instead of `[]byte("balances")` to save some disk space).
BalancesPrefix = []byte{0x02}
BalancesPrefix = collections.NewPrefix(2)
DenomAddressPrefix = collections.NewPrefix(3)
// SendEnabledPrefix is the prefix for the SendDisabled flags for a Denom.
SendEnabledPrefix = collections.NewPrefix(4)
@ -35,45 +33,27 @@ var (
ParamsKey = collections.NewPrefix(5)
)
// AddressAndDenomFromBalancesStore returns an account address and denom from a balances prefix
// store. The key must not contain the prefix BalancesPrefix as the prefix store
// iterator discards the actual prefix.
//
// If invalid key is passed, AddressAndDenomFromBalancesStore returns ErrInvalidKey.
func AddressAndDenomFromBalancesStore(key []byte) (sdk.AccAddress, string, error) {
if len(key) == 0 {
return nil, "", ErrInvalidKey
// NewBalanceCompatValueCodec is a codec for encoding Balances in a backwards compatible way
// with respect to the old format.
func NewBalanceCompatValueCodec() collcodec.ValueCodec[math.Int] {
return balanceCompatValueCodec{
sdk.IntValue,
}
}
kv.AssertKeyAtLeastLength(key, 1)
type balanceCompatValueCodec struct {
collcodec.ValueCodec[math.Int]
}
addrBound := int(key[0])
if len(key)-1 < addrBound {
return nil, "", ErrInvalidKey
func (v balanceCompatValueCodec) Decode(b []byte) (math.Int, error) {
i, err := v.ValueCodec.Decode(b)
if err == nil {
return i, nil
}
return key[1 : addrBound+1], string(key[addrBound+1:]), nil
}
// CreatePrefixedAccountStoreKey returns the key for the given account and denomination.
// This method can be used when performing an ABCI query for the balance of an account.
func CreatePrefixedAccountStoreKey(addr, denom []byte) []byte {
return append(CreateAccountBalancesPrefix(addr), denom...)
}
// CreateAccountBalancesPrefix creates the prefix for an account's balances.
func CreateAccountBalancesPrefix(addr []byte) []byte {
return append(BalancesPrefix, address.MustLengthPrefix(addr)...)
}
// CreateDenomAddressPrefix creates a prefix for a reverse index of denomination
// to account balance for that denomination.
func CreateDenomAddressPrefix(denom string) []byte {
// we add a "zero" byte at the end - null byte terminator, to allow prefix denom prefix
// scan. Setting it is not needed (key[last] = 0) - because this is the default.
key := make([]byte, len(DenomAddressPrefix)+len(denom)+1)
copy(key, DenomAddressPrefix)
copy(key[len(DenomAddressPrefix):], denom)
return key
c := new(sdk.Coin)
err = c.Unmarshal(b)
if err != nil {
return math.Int{}, err
}
return c.Amount, nil
}

View File

@ -1,70 +1,25 @@
package types_test
package types
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cosmossdk.io/collections/colltest"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/address"
"github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/stretchr/testify/require"
"testing"
)
func cloneAppend(bz, tail []byte) (res []byte) {
res = make([]byte, len(bz)+len(tail))
copy(res, bz)
copy(res[len(bz):], tail)
return
}
func TestAddressFromBalancesStore(t *testing.T) {
addr, err := sdk.AccAddressFromBech32("cosmos1n88uc38xhjgxzw9nwre4ep2c8ga4fjxcar6mn7")
require.NoError(t, err)
addrLen := len(addr)
require.Equal(t, 20, addrLen)
key := cloneAppend(address.MustLengthPrefix(addr), []byte("stake"))
tests := []struct {
name string
key []byte
wantErr bool
expectedKey sdk.AccAddress
}{
{"valid", key, false, addr},
{"#9111", []byte("\xff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), false, nil},
{"empty", []byte(""), true, nil},
{"invalid", []byte("3AA"), true, nil},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
addr, denom, err := types.AddressAndDenomFromBalancesStore(tc.key)
if tc.wantErr {
assert.Error(t, err)
assert.True(t, errors.Is(types.ErrInvalidKey, err))
} else {
assert.NoError(t, err)
}
if len(tc.expectedKey) > 0 {
assert.Equal(t, tc.expectedKey, addr)
assert.Equal(t, "stake", denom)
}
})
}
}
func TestCreateDenomAddressPrefix(t *testing.T) {
require := require.New(t)
key := types.CreateDenomAddressPrefix("")
require.Len(key, len(types.DenomAddressPrefix)+1)
require.Equal(append(types.DenomAddressPrefix, 0), key)
key = types.CreateDenomAddressPrefix("abc")
require.Len(key, len(types.DenomAddressPrefix)+4)
require.Equal(append(types.DenomAddressPrefix, 'a', 'b', 'c', 0), key)
func TestBalanceValueCodec(t *testing.T) {
c := NewBalanceCompatValueCodec()
t.Run("value codec implementation", func(t *testing.T) {
colltest.TestValueCodec(t, c, math.NewInt(100))
})
t.Run("legacy coin", func(t *testing.T) {
coin := sdk.NewInt64Coin("coin", 1000)
b, err := coin.Marshal()
require.NoError(t, err)
amt, err := c.Decode(b)
require.NoError(t, err)
require.Equal(t, coin.Amount, amt)
})
}

View File

@ -3,7 +3,6 @@ module github.com/cosmos/cosmos-sdk/x/circuit
go 1.20
require (
cosmossdk.io/store v0.1.0-alpha.1.0.20230328185921-37ba88872dbc
github.com/cosmos/cosmos-sdk v0.46.0-beta2.0.20230330094838-d21f58c638d5
github.com/cosmos/gogoproto v1.4.7
github.com/golang/protobuf v1.5.3
@ -22,6 +21,7 @@ require (
cosmossdk.io/errors v1.0.0-beta.7 // indirect
cosmossdk.io/log v1.0.0 // indirect
cosmossdk.io/math v1.0.0 // indirect
cosmossdk.io/store v0.1.0-alpha.1.0.20230328185921-37ba88872dbc // indirect
github.com/DataDog/zstd v1.5.2 // indirect
github.com/alecthomas/participle/v2 v2.0.0-alpha7 // indirect
github.com/armon/go-metrics v0.4.1 // indirect