diff --git a/CHANGELOG.md b/CHANGELOG.md index 330a5499f075..3706134b62a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features * (baseapp) [#205](https://github.com/crypto-org-chain/cosmos-sdk/pull/205) Add `TxExecutor` baseapp option, add `TxIndex`/`TxCount`/`MsgIndex`/`BlockGasUsed` fields to `Context, to support tx parallel execution. -* (baseapp) [#205](https://github.com/crypto-org-chain/cosmos-sdk/pull/205) Support mount object store in baseapp, add `ObjectStore` api in context.. +* (baseapp) [#206](https://github.com/crypto-org-chain/cosmos-sdk/pull/206) Support mount object store in baseapp, add `ObjectStore` api in context.. +* (bank) [#237](https://github.com/crypto-org-chain/cosmos-sdk/pull/237) Support virtual accounts in sending coins. ## [Unreleased-Upstream] diff --git a/simapp/app.go b/simapp/app.go index 3c27030af82d..6268e5a9daaf 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -146,6 +146,7 @@ type SimApp struct { // keys to access the substores keys map[string]*storetypes.KVStoreKey tkeys map[string]*storetypes.TransientStoreKey + okeys map[string]*storetypes.ObjectStoreKey // keepers AccountKeeper authkeeper.AccountKeeper @@ -266,6 +267,7 @@ func NewSimApp( } tkeys := storetypes.NewTransientStoreKeys(paramstypes.TStoreKey) + okeys := storetypes.NewObjectStoreKeys(banktypes.ObjectStoreKey) app := &SimApp{ BaseApp: bApp, legacyAmino: legacyAmino, @@ -274,6 +276,7 @@ func NewSimApp( interfaceRegistry: interfaceRegistry, keys: keys, tkeys: tkeys, + okeys: okeys, } app.ParamsKeeper = initParamsKeeper(appCodec, legacyAmino, keys[paramstypes.StoreKey], tkeys[paramstypes.TStoreKey]) @@ -288,6 +291,7 @@ func NewSimApp( app.BankKeeper = bankkeeper.NewBaseKeeper( appCodec, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], app.AccountKeeper, BlockedAddresses(), authtypes.NewModuleAddress(govtypes.ModuleName).String(), @@ -456,6 +460,7 @@ func NewSimApp( authz.ModuleName, ) app.ModuleManager.SetOrderEndBlockers( + banktypes.ModuleName, crisistypes.ModuleName, govtypes.ModuleName, stakingtypes.ModuleName, @@ -516,6 +521,7 @@ func NewSimApp( // initialize stores app.MountKVStores(keys) app.MountTransientStores(tkeys) + app.MountObjectStores(okeys) // initialize BaseApp app.SetInitChainer(app.InitChainer) diff --git a/testutil/context.go b/testutil/context.go index a43e96295ae2..6eed70f8523a 100644 --- a/testutil/context.go +++ b/testutil/context.go @@ -79,3 +79,17 @@ func DefaultContextWithDB(t testing.TB, key, tkey storetypes.StoreKey) TestConte return TestContext{ctx, db, cms} } + +func DefaultContextWithObjectStore(t testing.TB, key, tkey, okey storetypes.StoreKey) TestContext { + db := dbm.NewMemDB() + cms := store.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics()) + cms.MountStoreWithDB(key, storetypes.StoreTypeIAVL, db) + cms.MountStoreWithDB(tkey, storetypes.StoreTypeTransient, nil) + cms.MountStoreWithDB(okey, storetypes.StoreTypeObject, nil) + err := cms.LoadLatestVersion() + assert.NoError(t, err) + + ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Now()}, false, log.NewNopLogger()) + + return TestContext{ctx, db, cms} +} diff --git a/x/bank/keeper/collections_test.go b/x/bank/keeper/collections_test.go index e1af343cce56..364f923d3e29 100644 --- a/x/bank/keeper/collections_test.go +++ b/x/bank/keeper/collections_test.go @@ -26,7 +26,8 @@ import ( func TestBankStateCompatibility(t *testing.T) { key := storetypes.NewKVStoreKey(banktypes.StoreKey) - testCtx := testutil.DefaultContextWithDB(t, key, storetypes.NewTransientStoreKey("transient_test")) + okey := storetypes.NewObjectStoreKey(banktypes.ObjectStoreKey) + testCtx := testutil.DefaultContextWithObjectStore(t, key, storetypes.NewTransientStoreKey("transient_test"), okey) ctx := testCtx.Ctx.WithBlockHeader(cmtproto.Header{Time: cmttime.Now()}) encCfg := moduletestutil.MakeTestEncodingConfig() @@ -40,6 +41,7 @@ func TestBankStateCompatibility(t *testing.T) { k := keeper.NewBaseKeeper( encCfg.Codec, storeService, + okey, authKeeper, map[string]bool{accAddrs[4].String(): true}, authtypes.NewModuleAddress("gov").String(), diff --git a/x/bank/keeper/keeper.go b/x/bank/keeper/keeper.go index bfa45d23f64e..07f1253e728c 100644 --- a/x/bank/keeper/keeper.go +++ b/x/bank/keeper/keeper.go @@ -8,6 +8,7 @@ import ( errorsmod "cosmossdk.io/errors" "cosmossdk.io/log" "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -46,9 +47,13 @@ type Keeper interface { MintCoins(ctx context.Context, moduleName string, amt sdk.Coins) error BurnCoins(ctx context.Context, moduleName string, amt sdk.Coins) error + SendCoinsFromAccountToModuleVirtual(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + DelegateCoins(ctx context.Context, delegatorAddr, moduleAccAddr sdk.AccAddress, amt sdk.Coins) error UndelegateCoins(ctx context.Context, moduleAccAddr, delegatorAddr sdk.AccAddress, amt sdk.Coins) error + CreditVirtualAccounts(ctx context.Context) error + types.QueryServer } @@ -84,6 +89,7 @@ func (k BaseKeeper) GetPaginatedTotalSupply(ctx context.Context, pagination *que func NewBaseKeeper( cdc codec.BinaryCodec, storeService store.KVStoreService, + objStoreKey storetypes.StoreKey, ak types.AccountKeeper, blockedAddrs map[string]bool, authority string, @@ -97,7 +103,7 @@ func NewBaseKeeper( logger = logger.With(log.ModuleKey, "x/"+types.ModuleName) return BaseKeeper{ - BaseSendKeeper: NewBaseSendKeeper(cdc, storeService, ak, blockedAddrs, authority, logger), + BaseSendKeeper: NewBaseSendKeeper(cdc, storeService, objStoreKey, ak, blockedAddrs, authority, logger), ak: ak, cdc: cdc, storeService: storeService, diff --git a/x/bank/keeper/keeper_test.go b/x/bank/keeper/keeper_test.go index 4bb11862eb62..bea5e9fb8bb8 100644 --- a/x/bank/keeper/keeper_test.go +++ b/x/bank/keeper/keeper_test.go @@ -130,7 +130,8 @@ func TestKeeperTestSuite(t *testing.T) { func (suite *KeeperTestSuite) SetupTest() { key := storetypes.NewKVStoreKey(banktypes.StoreKey) - testCtx := testutil.DefaultContextWithDB(suite.T(), key, storetypes.NewTransientStoreKey("transient_test")) + okey := storetypes.NewObjectStoreKey(banktypes.ObjectStoreKey) + testCtx := testutil.DefaultContextWithObjectStore(suite.T(), key, storetypes.NewTransientStoreKey("transient_test"), okey) ctx := testCtx.Ctx.WithBlockHeader(cmtproto.Header{Time: cmttime.Now()}) encCfg := moduletestutil.MakeTestEncodingConfig() @@ -145,6 +146,7 @@ func (suite *KeeperTestSuite) SetupTest() { suite.bankKeeper = keeper.NewBaseKeeper( encCfg.Codec, storeService, + okey, suite.authKeeper, map[string]bool{accAddrs[4].String(): true}, authtypes.NewModuleAddress(govtypes.ModuleName).String(), @@ -196,6 +198,11 @@ func (suite *KeeperTestSuite) mockSendCoinsFromAccountToModule(acc *authtypes.Ba suite.authKeeper.EXPECT().HasAccount(suite.ctx, moduleAcc.GetAddress()).Return(true) } +func (suite *KeeperTestSuite) mockSendCoinsFromAccountToModuleVirtual(acc *authtypes.BaseAccount, moduleAcc *authtypes.ModuleAccount) { + suite.authKeeper.EXPECT().GetModuleAccount(suite.ctx, moduleAcc.Name).Return(moduleAcc) + suite.authKeeper.EXPECT().GetAccount(suite.ctx, acc.GetAddress()).Return(acc) +} + func (suite *KeeperTestSuite) mockSendCoins(ctx context.Context, sender sdk.AccountI, receiver sdk.AccAddress) { suite.authKeeper.EXPECT().GetAccount(ctx, sender.GetAddress()).Return(sender) suite.authKeeper.EXPECT().HasAccount(ctx, receiver).Return(true) @@ -316,6 +323,7 @@ func (suite *KeeperTestSuite) TestGetAuthority() { return keeper.NewBaseKeeper( moduletestutil.MakeTestEncodingConfig().Codec, storeService, + nil, suite.authKeeper, nil, authority, @@ -632,6 +640,27 @@ func (suite *KeeperTestSuite) TestSendCoinsNewAccount() { require.Equal(acc1Balances, updatedAcc1Bal) } +func (suite *KeeperTestSuite) TestSendCoinsVirtual() { + ctx := suite.ctx + require := suite.Require() + keeper := suite.bankKeeper + sdkCtx := sdk.UnwrapSDKContext(ctx) + acc0 := authtypes.NewBaseAccountWithAddress(accAddrs[0]) + + balances := sdk.NewCoins(newFooCoin(100), newBarCoin(50)) + suite.mockFundAccount(accAddrs[0]) + require.NoError(banktestutil.FundAccount(ctx, suite.bankKeeper, accAddrs[0], balances)) + + sendAmt := sdk.NewCoins(newFooCoin(50), newBarCoin(50)) + suite.mockSendCoinsFromAccountToModuleVirtual(acc0, burnerAcc) + require.NoError( + keeper.SendCoinsFromAccountToModuleVirtual(sdkCtx, accAddrs[0], authtypes.Burner, sendAmt), + ) + + suite.authKeeper.EXPECT().HasAccount(suite.ctx, burnerAcc.GetAddress()).Return(true) + require.NoError(keeper.CreditVirtualAccounts(ctx)) +} + func (suite *KeeperTestSuite) TestInputOutputNewAccount() { ctx := suite.ctx require := suite.Require() diff --git a/x/bank/keeper/send.go b/x/bank/keeper/send.go index 3bcc6d315c33..82fc93b1bed7 100644 --- a/x/bank/keeper/send.go +++ b/x/bank/keeper/send.go @@ -9,6 +9,7 @@ import ( errorsmod "cosmossdk.io/errors" "cosmossdk.io/log" "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/telemetry" @@ -60,6 +61,7 @@ type BaseSendKeeper struct { ak types.AccountKeeper storeService store.KVStoreService logger log.Logger + objStoreKey storetypes.StoreKey // list of addresses that are restricted from receiving transactions blockedAddrs map[string]bool @@ -74,6 +76,7 @@ type BaseSendKeeper struct { func NewBaseSendKeeper( cdc codec.BinaryCodec, storeService store.KVStoreService, + objStoreKey storetypes.StoreKey, ak types.AccountKeeper, blockedAddrs map[string]bool, authority string, @@ -88,6 +91,7 @@ func NewBaseSendKeeper( cdc: cdc, ak: ak, storeService: storeService, + objStoreKey: objStoreKey, blockedAddrs: blockedAddrs, authority: authority, logger: logger, @@ -244,7 +248,7 @@ func (k BaseSendKeeper) SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccA ), sdk.NewEvent( sdk.EventTypeMessage, - sdk.NewAttribute(types.AttributeKeySender, fromAddr.String()), + sdk.NewAttribute(types.AttributeKeySender, fromAddrString), ), }) diff --git a/x/bank/keeper/virtual.go b/x/bank/keeper/virtual.go new file mode 100644 index 000000000000..a73b8496ddbb --- /dev/null +++ b/x/bank/keeper/virtual.go @@ -0,0 +1,127 @@ +package keeper + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/hex" + "fmt" + + errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/telemetry" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +func (k BaseSendKeeper) SendCoinsFromAccountToModuleVirtual( + ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins, +) error { + recipientAcc := k.ak.GetModuleAccount(ctx, recipientModule) + if recipientAcc == nil { + return errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", recipientModule) + } + + return k.SendCoinsVirtual(ctx, senderAddr, recipientAcc.GetAddress(), amt) +} + +// SendCoinsVirtual accumulate the recipient's coins in a per-transaction transient state, +// which are sumed up and added to the real account at the end of block. +// Events are emiited the same as normal send. +func (k BaseSendKeeper) SendCoinsVirtual(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error { + var err error + err = k.subUnlockedCoins(ctx, fromAddr, amt) + if err != nil { + return err + } + + toAddr, err = k.sendRestriction.apply(ctx, fromAddr, toAddr, amt) + if err != nil { + return err + } + + k.addVirtualCoins(ctx, toAddr, amt) + + // bech32 encoding is expensive! Only do it once for fromAddr + fromAddrString := fromAddr.String() + sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeTransfer, + sdk.NewAttribute(types.AttributeKeyRecipient, toAddr.String()), + sdk.NewAttribute(types.AttributeKeySender, fromAddrString), + sdk.NewAttribute(sdk.AttributeKeyAmount, amt.String()), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(types.AttributeKeySender, fromAddrString), + ), + }) + + return nil +} + +func (k BaseSendKeeper) addVirtualCoins(ctx context.Context, addr sdk.AccAddress, amt sdk.Coins) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + store := sdkCtx.ObjectStore(k.objStoreKey) + + key := make([]byte, len(addr)+8) + copy(key, addr) + binary.BigEndian.PutUint64(key[len(addr):], uint64(sdkCtx.TxIndex())) + + var coins sdk.Coins + value := store.Get(key) + if value != nil { + coins = value.(sdk.Coins) + } + coins = coins.Add(amt...) + store.Set(key, coins) +} + +// CreditVirtualAccounts sum up the transient coins and add them to the real account, +// should be called at end blocker. +func (k BaseSendKeeper) CreditVirtualAccounts(ctx context.Context) error { + store := sdk.UnwrapSDKContext(ctx).ObjectStore(k.objStoreKey) + + var toAddr sdk.AccAddress + var sum sdk.Coins + flushCurrentAddr := func() { + if sum.IsZero() { + return + } + + k.addCoins(ctx, toAddr, sum) + sum = sum[:0] + + // Create account if recipient does not exist. + // + // NOTE: This should ultimately be removed in favor a more flexible approach + // such as delegated fee messages. + accExists := k.ak.HasAccount(ctx, toAddr) + if !accExists { + defer telemetry.IncrCounter(1, "new", "account") + k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, toAddr)) + } + } + + it := store.Iterator(nil, nil) + defer it.Close() + for ; it.Valid(); it.Next() { + if len(it.Key()) <= 8 { + return fmt.Errorf("unexpected key length: %s", hex.EncodeToString(it.Key())) + } + + addr := it.Key()[:len(it.Key())-8] + if !bytes.Equal(toAddr, addr) { + flushCurrentAddr() + toAddr = addr + } + + coins := it.Value().(sdk.Coins) + // TODO more efficient coins sum + sum = sum.Add(coins...) + } + + flushCurrentAddr() + return nil +} diff --git a/x/bank/module.go b/x/bank/module.go index 315a6ecfc744..ff5e21331f93 100644 --- a/x/bank/module.go +++ b/x/bank/module.go @@ -15,6 +15,7 @@ import ( corestore "cosmossdk.io/core/store" "cosmossdk.io/depinject" "cosmossdk.io/log" + storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" @@ -43,7 +44,8 @@ var ( _ module.HasServices = AppModule{} _ module.HasInvariants = AppModule{} - _ appmodule.AppModule = AppModule{} + _ appmodule.AppModule = AppModule{} + _ appmodule.HasEndBlocker = AppModule{} ) // AppModuleBasic defines the basic application module used by the bank module. @@ -195,6 +197,10 @@ func (am AppModule) WeightedOperations(simState module.SimulationState) []simtyp ) } +func (am AppModule) EndBlock(ctx context.Context) error { + return am.keeper.CreditVirtualAccounts(ctx) +} + // App Wiring Setup func init() { @@ -210,6 +216,7 @@ type ModuleInputs struct { Cdc codec.Codec StoreService corestore.KVStoreService Logger log.Logger + ObjStoreKey *storetypes.ObjectStoreKey AccountKeeper types.AccountKeeper @@ -249,6 +256,7 @@ func ProvideModule(in ModuleInputs) ModuleOutputs { bankKeeper := keeper.NewBaseKeeper( in.Cdc, in.StoreService, + in.ObjStoreKey, in.AccountKeeper, blockedAddresses, authority.String(), diff --git a/x/bank/types/keys.go b/x/bank/types/keys.go index b4ea683d4b69..fab1a1eb61d6 100644 --- a/x/bank/types/keys.go +++ b/x/bank/types/keys.go @@ -17,6 +17,9 @@ const ( // RouterKey defines the module's message routing key RouterKey = ModuleName + + // ObjectStoreKey defines the store name for the object store + ObjectStoreKey = "obj_" + ModuleName ) // KVStore keys