Skip to content

Commit

Permalink
Problem: no efficient way to collect fee
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
yihuang committed Mar 27, 2024
1 parent 4d32911 commit e03100f
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 6 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
6 changes: 6 additions & 0 deletions simapp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -266,6 +267,7 @@ func NewSimApp(
}

tkeys := storetypes.NewTransientStoreKeys(paramstypes.TStoreKey)
okeys := storetypes.NewObjectStoreKeys(banktypes.ObjectStoreKey)
app := &SimApp{
BaseApp: bApp,
legacyAmino: legacyAmino,
Expand All @@ -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])
Expand All @@ -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(),
Expand Down Expand Up @@ -456,6 +460,7 @@ func NewSimApp(
authz.ModuleName,
)
app.ModuleManager.SetOrderEndBlockers(
banktypes.ModuleName,
crisistypes.ModuleName,
govtypes.ModuleName,
stakingtypes.ModuleName,
Expand Down Expand Up @@ -516,6 +521,7 @@ func NewSimApp(
// initialize stores
app.MountKVStores(keys)
app.MountTransientStores(tkeys)
app.MountObjectStores(okeys)

// initialize BaseApp
app.SetInitChainer(app.InitChainer)
Expand Down
14 changes: 14 additions & 0 deletions testutil/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
4 changes: 3 additions & 1 deletion x/bank/keeper/collections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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(),
Expand Down
8 changes: 7 additions & 1 deletion x/bank/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
31 changes: 30 additions & 1 deletion x/bank/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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(),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -316,6 +323,7 @@ func (suite *KeeperTestSuite) TestGetAuthority() {
return keeper.NewBaseKeeper(
moduletestutil.MakeTestEncodingConfig().Codec,
storeService,
nil,
suite.authKeeper,
nil,
authority,
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 5 additions & 1 deletion x/bank/keeper/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -88,6 +91,7 @@ func NewBaseSendKeeper(
cdc: cdc,
ak: ak,
storeService: storeService,
objStoreKey: objStoreKey,
blockedAddrs: blockedAddrs,
authority: authority,
logger: logger,
Expand Down Expand Up @@ -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),
),
})

Expand Down
127 changes: 127 additions & 0 deletions x/bank/keeper/virtual.go
Original file line number Diff line number Diff line change
@@ -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)

Check warning

Code scanning / gosec

Errors unhandled. Warning

Errors unhandled.
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
}
Loading

0 comments on commit e03100f

Please sign in to comment.