From f04a9c944413b93aa80b8c85d004a3d5310038d2 Mon Sep 17 00:00:00 2001 From: HuangYi Date: Wed, 27 Mar 2024 17:43:39 +0800 Subject: [PATCH] Problem: no efficient way to collect fee Solution: - support an idea of virtual account in bank module, where the incoming coins are accumulated in a per-tx object store first, then accumulate and credit to the real account at end blocker. it's nesserary to support parallel tx execution, where we try not to access shared states. more efficient sum support SendCoinsFromModuleToAccountVirtual fix test fix test fix lint fix test fix test fix test fix test fix test fix mock keeper try fix lint try fix lint reuse code try fix linter Update x/bank/keeper/send.go Signed-off-by: yihuang algin panic call fix error handling try fix lint nolintlint generate falst postiive --- .golangci.yml | 1 - CHANGELOG.md | 3 +- baseapp/baseapp.go | 3 + runtime/module.go | 7 + simapp/app.go | 6 + simapp/app_config.go | 1 + .../bank/keeper/deterministic_test.go | 4 +- .../distribution/keeper/msg_server_test.go | 6 +- .../evidence/keeper/infraction_test.go | 6 +- tests/integration/gov/keeper/keeper_test.go | 6 +- .../slashing/keeper/keeper_test.go | 4 +- .../integration/staking/keeper/common_test.go | 6 +- .../staking/keeper/determinstic_test.go | 4 +- testutil/context.go | 14 ++ testutil/integration/example_test.go | 4 +- testutil/integration/router.go | 6 +- x/bank/keeper/collections_test.go | 4 +- x/bank/keeper/keeper.go | 9 +- x/bank/keeper/keeper_test.go | 47 ++++- x/bank/keeper/send.go | 17 +- x/bank/keeper/virtual.go | 178 ++++++++++++++++++ x/bank/module.go | 10 +- x/bank/types/keys.go | 3 + x/gov/testutil/expected_keepers_mocks.go | 23 +++ 24 files changed, 353 insertions(+), 19 deletions(-) create mode 100644 x/bank/keeper/virtual.go diff --git a/.golangci.yml b/.golangci.yml index 7f51609abf1d..5ad15c222fb2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -27,7 +27,6 @@ linters: - ineffassign - misspell - nakedret - - nolintlint - staticcheck - revive - stylecheck 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/baseapp/baseapp.go b/baseapp/baseapp.go index eeb8a414d643..1b39ffba0bf3 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -305,6 +305,9 @@ func (app *BaseApp) MountStores(keys ...storetypes.StoreKey) { case *storetypes.MemoryStoreKey: app.MountStore(key, storetypes.StoreTypeMemory) + case *storetypes.ObjectStoreKey: + app.MountStore(key, storetypes.StoreTypeObject) + default: panic(fmt.Sprintf("Unrecognized store key type :%T", key)) } diff --git a/runtime/module.go b/runtime/module.go index cdc6a1a3a33f..148efb40fa8d 100644 --- a/runtime/module.go +++ b/runtime/module.go @@ -67,6 +67,7 @@ func init() { ProvideKVStoreKey, ProvideTransientStoreKey, ProvideMemoryStoreKey, + ProvideObjectStoreKey, ProvideGenesisTxHandler, ProvideKVStoreService, ProvideMemoryStoreService, @@ -222,6 +223,12 @@ func ProvideMemoryStoreKey(key depinject.ModuleKey, app *AppBuilder) *storetypes return storeKey } +func ProvideObjectStoreKey(key depinject.ModuleKey, app *AppBuilder) *storetypes.ObjectStoreKey { + storeKey := storetypes.NewObjectStoreKey(fmt.Sprintf("object:%s", key.Name())) + registerStoreKey(app, storeKey) + return storeKey +} + func ProvideGenesisTxHandler(appBuilder *AppBuilder) genesis.TxHandler { return appBuilder.app } 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/simapp/app_config.go b/simapp/app_config.go index 2b11313e60dd..d799fe52e283 100644 --- a/simapp/app_config.go +++ b/simapp/app_config.go @@ -122,6 +122,7 @@ var ( authz.ModuleName, }, EndBlockers: []string{ + banktypes.ModuleName, crisistypes.ModuleName, govtypes.ModuleName, stakingtypes.ModuleName, diff --git a/tests/integration/bank/keeper/deterministic_test.go b/tests/integration/bank/keeper/deterministic_test.go index 86bce291117c..fe4c6b4b2e42 100644 --- a/tests/integration/bank/keeper/deterministic_test.go +++ b/tests/integration/bank/keeper/deterministic_test.go @@ -64,10 +64,11 @@ type deterministicFixture struct { func initDeterministicFixture(t *testing.T) *deterministicFixture { keys := storetypes.NewKVStoreKeys(authtypes.StoreKey, banktypes.StoreKey) + okeys := storetypes.NewObjectStoreKeys(banktypes.ObjectStoreKey) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, bank.AppModuleBasic{}).Codec logger := log.NewTestLogger(t) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) @@ -93,6 +94,7 @@ func initDeterministicFixture(t *testing.T) *deterministicFixture { bankKeeper := keeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), diff --git a/tests/integration/distribution/keeper/msg_server_test.go b/tests/integration/distribution/keeper/msg_server_test.go index 86a2d8e626e2..fb6e82ad755d 100644 --- a/tests/integration/distribution/keeper/msg_server_test.go +++ b/tests/integration/distribution/keeper/msg_server_test.go @@ -61,10 +61,13 @@ func initFixture(t testing.TB) *fixture { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, distrtypes.StoreKey, stakingtypes.StoreKey, ) + okeys := storetypes.NewObjectStoreKeys( + banktypes.ObjectStoreKey, + ) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, distribution.AppModuleBasic{}).Codec logger := log.NewTestLogger(t) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, types.Header{}, true, logger) @@ -92,6 +95,7 @@ func initFixture(t testing.TB) *fixture { bankKeeper := bankkeeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), diff --git a/tests/integration/evidence/keeper/infraction_test.go b/tests/integration/evidence/keeper/infraction_test.go index 0c17797754bf..596bca82418b 100644 --- a/tests/integration/evidence/keeper/infraction_test.go +++ b/tests/integration/evidence/keeper/infraction_test.go @@ -83,10 +83,13 @@ func initFixture(t testing.TB) *fixture { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, paramtypes.StoreKey, consensusparamtypes.StoreKey, evidencetypes.StoreKey, stakingtypes.StoreKey, slashingtypes.StoreKey, ) + okeys := storetypes.NewObjectStoreKeys( + banktypes.ObjectStoreKey, + ) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, evidence.AppModuleBasic{}).Codec logger := log.NewTestLogger(t) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) @@ -114,6 +117,7 @@ func initFixture(t testing.TB) *fixture { bankKeeper := bankkeeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), diff --git a/tests/integration/gov/keeper/keeper_test.go b/tests/integration/gov/keeper/keeper_test.go index 4e385e8f0d70..1d8001878588 100644 --- a/tests/integration/gov/keeper/keeper_test.go +++ b/tests/integration/gov/keeper/keeper_test.go @@ -52,10 +52,13 @@ func initFixture(t testing.TB) *fixture { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, distrtypes.StoreKey, stakingtypes.StoreKey, types.StoreKey, ) + okeys := storetypes.NewObjectStoreKeys( + banktypes.ObjectStoreKey, + ) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, bank.AppModuleBasic{}, gov.AppModuleBasic{}).Codec logger := log.NewTestLogger(t) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) @@ -85,6 +88,7 @@ func initFixture(t testing.TB) *fixture { bankKeeper := bankkeeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), diff --git a/tests/integration/slashing/keeper/keeper_test.go b/tests/integration/slashing/keeper/keeper_test.go index d43c07f1df85..74aacdf36859 100644 --- a/tests/integration/slashing/keeper/keeper_test.go +++ b/tests/integration/slashing/keeper/keeper_test.go @@ -54,10 +54,11 @@ func initFixture(t testing.TB) *fixture { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, slashingtypes.StoreKey, stakingtypes.StoreKey, ) + okeys := storetypes.NewObjectStoreKeys(banktypes.ObjectStoreKey) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}).Codec logger := log.NewTestLogger(t) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) @@ -85,6 +86,7 @@ func initFixture(t testing.TB) *fixture { bankKeeper := bankkeeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), diff --git a/tests/integration/staking/keeper/common_test.go b/tests/integration/staking/keeper/common_test.go index f4958b1b86da..b5e2815efb27 100644 --- a/tests/integration/staking/keeper/common_test.go +++ b/tests/integration/staking/keeper/common_test.go @@ -95,10 +95,13 @@ func initFixture(t testing.TB) *fixture { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, types.StoreKey, ) + okeys := storetypes.NewObjectStoreKeys( + banktypes.ObjectStoreKey, + ) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, staking.AppModuleBasic{}).Codec logger := log.NewTestLogger(t) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, cmtprototypes.Header{}, true, logger) @@ -127,6 +130,7 @@ func initFixture(t testing.TB) *fixture { bankKeeper := bankkeeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), diff --git a/tests/integration/staking/keeper/determinstic_test.go b/tests/integration/staking/keeper/determinstic_test.go index c6c0f6377d37..99f2a86a47ee 100644 --- a/tests/integration/staking/keeper/determinstic_test.go +++ b/tests/integration/staking/keeper/determinstic_test.go @@ -68,10 +68,11 @@ func initDeterministicFixture(t *testing.T) *deterministicFixture { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey, ) + okeys := storetypes.NewObjectStoreKeys(banktypes.ObjectStoreKey) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, distribution.AppModuleBasic{}).Codec logger := log.NewTestLogger(t) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) @@ -100,6 +101,7 @@ func initDeterministicFixture(t *testing.T) *deterministicFixture { bankKeeper := bankkeeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), 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/testutil/integration/example_test.go b/testutil/integration/example_test.go index b395324c1917..698bcbd5d4c4 100644 --- a/testutil/integration/example_test.go +++ b/testutil/integration/example_test.go @@ -37,7 +37,7 @@ func Example() { // replace the logger by testing values in a real test case (e.g. log.NewTestLogger(t)) logger := log.NewNopLogger() - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, nil, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) accountKeeper := authkeeper.NewAccountKeeper( @@ -126,7 +126,7 @@ func Example_oneModule() { // replace the logger by testing values in a real test case (e.g. log.NewTestLogger(t)) logger := log.NewLogger(io.Discard) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, nil, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) accountKeeper := authkeeper.NewAccountKeeper( diff --git a/testutil/integration/router.go b/testutil/integration/router.go index 4270d493ad8c..033f4f9186b0 100644 --- a/testutil/integration/router.go +++ b/testutil/integration/router.go @@ -177,7 +177,7 @@ func (app *App) QueryHelper() *baseapp.QueryServiceTestHelper { } // CreateMultiStore is a helper for setting up multiple stores for provided modules. -func CreateMultiStore(keys map[string]*storetypes.KVStoreKey, logger log.Logger) storetypes.CommitMultiStore { +func CreateMultiStore(keys map[string]*storetypes.KVStoreKey, okeys map[string]*storetypes.ObjectStoreKey, logger log.Logger) storetypes.CommitMultiStore { db := dbm.NewMemDB() cms := store.NewCommitMultiStore(db, logger, metrics.NewNoOpMetrics()) @@ -185,6 +185,10 @@ func CreateMultiStore(keys map[string]*storetypes.KVStoreKey, logger log.Logger) cms.MountStoreWithDB(keys[key], storetypes.StoreTypeIAVL, db) } + for key := range okeys { + cms.MountStoreWithDB(okeys[key], storetypes.StoreTypeObject, nil) + } + _ = cms.LoadLatestVersion() return 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..26dceca9b66b 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,14 @@ 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 + SendCoinsFromModuleToAccountVirtual(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, 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 +90,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 +104,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..d3ed16599137 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,16 @@ 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) mockSendCoinsFromModuleToAccountVirtual(moduleAcc *authtypes.ModuleAccount, accAddr sdk.AccAddress) { + suite.authKeeper.EXPECT().GetModuleAddress(moduleAcc.Name).Return(moduleAcc.GetAddress()) + suite.authKeeper.EXPECT().HasAccount(suite.ctx, accAddr).Return(true) +} + 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 +328,7 @@ func (suite *KeeperTestSuite) TestGetAuthority() { return keeper.NewBaseKeeper( moduletestutil.MakeTestEncodingConfig().Codec, storeService, + nil, suite.authKeeper, nil, authority, @@ -632,6 +645,38 @@ 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]) + feeDenom1 := "fee1" + feeDenom2 := "fee2" + + balances := sdk.NewCoins(sdk.NewInt64Coin(feeDenom1, 100), sdk.NewInt64Coin(feeDenom2, 100)) + suite.mockFundAccount(accAddrs[0]) + require.NoError(banktestutil.FundAccount(ctx, suite.bankKeeper, accAddrs[0], balances)) + + sendAmt := sdk.NewCoins(sdk.NewInt64Coin(feeDenom1, 50), sdk.NewInt64Coin(feeDenom2, 50)) + suite.mockSendCoinsFromAccountToModuleVirtual(acc0, burnerAcc) + require.NoError( + keeper.SendCoinsFromAccountToModuleVirtual(sdkCtx, accAddrs[0], authtypes.Burner, sendAmt), + ) + + refundAmt := sdk.NewCoins(sdk.NewInt64Coin(feeDenom1, 25), sdk.NewInt64Coin(feeDenom2, 25)) + suite.mockSendCoinsFromModuleToAccountVirtual(burnerAcc, accAddrs[0]) + require.NoError( + keeper.SendCoinsFromModuleToAccountVirtual(sdkCtx, authtypes.Burner, accAddrs[0], refundAmt), + ) + + suite.authKeeper.EXPECT().HasAccount(suite.ctx, burnerAcc.GetAddress()).Return(true) + require.NoError(keeper.CreditVirtualAccounts(ctx)) + + require.Equal(math.NewInt(25), keeper.GetBalance(suite.ctx, burnerAcc.GetAddress(), feeDenom1).Amount) + require.Equal(math.NewInt(25), keeper.GetBalance(suite.ctx, burnerAcc.GetAddress(), feeDenom2).Amount) +} + 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..7f9bd6016506 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, @@ -222,6 +226,12 @@ func (k BaseSendKeeper) SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccA return err } + k.ensureAccountCreated(ctx, toAddr) + k.emitSendCoinsEvents(ctx, fromAddr, toAddr, amt) + return nil +} + +func (k BaseSendKeeper) ensureAccountCreated(ctx context.Context, toAddr sdk.AccAddress) { // Create account if recipient does not exist. // // NOTE: This should ultimately be removed in favor a more flexible approach @@ -231,7 +241,10 @@ func (k BaseSendKeeper) SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccA defer telemetry.IncrCounter(1, "new", "account") k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, toAddr)) } +} +// emitSendCoinsEvents emit send coins events. +func (k BaseSendKeeper) emitSendCoinsEvents(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) { // bech32 encoding is expensive! Only do it once for fromAddr fromAddrString := fromAddr.String() sdkCtx := sdk.UnwrapSDKContext(ctx) @@ -244,11 +257,9 @@ 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), ), }) - - return nil } // subUnlockedCoins removes the unlocked amt coins of the given account. An error is diff --git a/x/bank/keeper/virtual.go b/x/bank/keeper/virtual.go new file mode 100644 index 000000000000..f7288e7e0b52 --- /dev/null +++ b/x/bank/keeper/virtual.go @@ -0,0 +1,178 @@ +package keeper + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/hex" + "fmt" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// SendCoinsFromAccountToModuleVirtual sends coins from account to a virtual module account. +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 { + panic(errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", recipientModule)) + } + + return k.SendCoinsToVirtual(ctx, senderAddr, recipientAcc.GetAddress(), amt) +} + +// SendCoinsFromModuleToAccountVirtual sends coins from account to a virtual module account. +func (k BaseSendKeeper) SendCoinsFromModuleToAccountVirtual( + ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, +) error { + senderAddr := k.ak.GetModuleAddress(senderModule) + if senderAddr == nil { + panic(errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", senderModule)) + } + + if k.BlockedAddr(recipientAddr) { + return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", recipientAddr) + } + + return k.SendCoinsFromVirtual(ctx, senderAddr, recipientAddr, amt) +} + +// SendCoinsToVirtual 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) SendCoinsToVirtual(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) + k.emitSendCoinsEvents(ctx, fromAddr, toAddr, amt) + return nil +} + +// SendCoinsFromVirtual deduct coins from virtual from account and send to recipient account. +func (k BaseSendKeeper) SendCoinsFromVirtual(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error { + var err error + err = k.subVirtualCoins(ctx, fromAddr, amt) + if err != nil { + return err + } + + toAddr, err = k.sendRestriction.apply(ctx, fromAddr, toAddr, amt) + if err != nil { + return err + } + + err = k.addCoins(ctx, toAddr, amt) + if err != nil { + return err + } + + k.ensureAccountCreated(ctx, toAddr) + k.emitSendCoinsEvents(ctx, fromAddr, toAddr, amt) + 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) +} + +func (k BaseSendKeeper) subVirtualCoins(ctx context.Context, addr sdk.AccAddress, amt sdk.Coins) error { + 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())) + + value := store.Get(key) + if value == nil { + return errorsmod.Wrapf( + sdkerrors.ErrInsufficientFunds, + "spendable balance 0 is smaller than %s", + amt, + ) + } + spendable := value.(sdk.Coins) + balance, hasNeg := spendable.SafeSub(amt...) + if hasNeg { + return errorsmod.Wrapf( + sdkerrors.ErrInsufficientFunds, + "spendable balance %s is smaller than %s", + spendable, amt, + ) + } + if balance.IsZero() { + store.Delete(key) + } else { + store.Set(key, balance) + } + + return nil +} + +// 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 + sum := sdk.NewMapCoins(nil) + flushCurrentAddr := func() error { + if len(sum) == 0 { + // nothing to flush + return nil + } + + if err := k.addCoins(ctx, toAddr, sum.ToCoins()); err != nil { + return err + } + clear(sum) + + k.ensureAccountCreated(ctx, toAddr) + return nil + } + + 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) { + if err := flushCurrentAddr(); err != nil { + return err + } + toAddr = addr + } + + sum.Add(it.Value().(sdk.Coins)...) + } + + return flushCurrentAddr() +} 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..485d1cdb9ef0 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 = "object:" + ModuleName ) // KVStore keys diff --git a/x/gov/testutil/expected_keepers_mocks.go b/x/gov/testutil/expected_keepers_mocks.go index 5970d3822f6a..43d2e05fd41e 100644 --- a/x/gov/testutil/expected_keepers_mocks.go +++ b/x/gov/testutil/expected_keepers_mocks.go @@ -784,6 +784,22 @@ func (m *MockBankKeeper) SendCoinsFromAccountToModule(ctx context.Context, sende return ret0 } +// SendCoinsFromAccountToModuleVirtual mocks base method. +func (m *MockBankKeeper) SendCoinsFromAccountToModuleVirtual(ctx context.Context, senderAddr types.AccAddress, recipientModule string, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromAccountToModuleVirtual", ctx, senderAddr, recipientModule, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromModuleToAccountVirtual mocks base method. +func (m *MockBankKeeper) SendCoinsFromModuleToAccountVirtual(ctx context.Context, senderModule string, recipientAddr types.AccAddress, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromModuleToAccountVirtual", ctx, senderModule, recipientAddr, amt) + ret0, _ := ret[0].(error) + return ret0 +} + // SendCoinsFromAccountToModule indicates an expected call of SendCoinsFromAccountToModule. func (mr *MockBankKeeperMockRecorder) SendCoinsFromAccountToModule(ctx, senderAddr, recipientModule, amt interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() @@ -812,6 +828,13 @@ func (m *MockBankKeeper) SendCoinsFromModuleToModule(ctx context.Context, sender return ret0 } +func (m *MockBankKeeper) CreditVirtualAccounts(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreditVirtualAccounts", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + // SendCoinsFromModuleToModule indicates an expected call of SendCoinsFromModuleToModule. func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToModule(ctx, senderModule, recipientModule, amt interface{}) *gomock.Call { mr.mock.ctrl.T.Helper()