From 55e55e63db3ad65348f171d69f0310426520736a Mon Sep 17 00:00:00 2001 From: Sujong Lee Date: Wed, 6 Nov 2024 17:23:43 +0900 Subject: [PATCH] feat: apply cronos (#208) * Problem: parallel tx execution is not supported (#205) add basic support in sdk: - add a TxExecutor baseapp option - add TxIndex/TxCount/MsgIndex in context Update CHANGELOG.md Signed-off-by: yihuang fix misspell fix lint run gci fix lint gci seems not compatible with gofumpt * Support object store (#206) generic interface generic btree generic cachekv generic transient store support ObjStore changelog Update CHANGELOG.md Signed-off-by: yihuang object store key Apply review suggestions fix merge conflict fix snapshot revert dependers * prefix store support object store (#236) * Problem: fee collection not compatible with parallel execution (#237) * 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 --------- Signed-off-by: yihuang * Problem: MultiStore interface is bloated (#240) * Problem: MultiStore interface is bloated Solution: - Split out specialied methods from it, keeping the MultiStore generic * Update store/CHANGELOG.md Signed-off-by: yihuang --------- Signed-off-by: yihuang * Problem: no API to use the new CoW branch store (#243) * Support RunAtomic API * add unit test * Problem: block gas used not set in context (#252) Solution: - fix the way context is updated * Problem: invalid tx returns negative gas wanted (#253) Solution: - add checks * Problem: signature verification result not cache between incarnations of same tx (#565) * Problem: signature verification result not cache between incarnations of same tx Closes: #564 Solution: - introduce incarnation cache that's shared between incarnations of the same tx * Update CHANGELOG.md Signed-off-by: yihuang * Update types/context.go Signed-off-by: yihuang * Update types/context.go Signed-off-by: yihuang * fix nil convert --------- Signed-off-by: yihuang * Problem: gas consumed differs after enabled cache (#570) * Problem: gas consumed differs after enabled cache * fix test * Revert "Problem: gas consumed differs after enabled cache" This reverts commit f33944ee849c66f1b51d5d29e6e4f3b6ce41797a. * Revert "Problem: signature verification result not cache between incarnations of same tx (#565)" This reverts commit 5a1594f17924388dc2530bd8143ca2b04655fad4. * keep interface * Problem: tx executor can't do dependency analysis (#744) * Problem: tx executor can't do dependency analysis Solution: - change the api to allow static analysis on tx body * fix * changelog * cleanup * Update CHANGELOG.md Signed-off-by: yihuang --------- Signed-off-by: yihuang * Problem: can't extend abci listeners in streaming manager (#269) Solution: - add API StreamingManager to allow extending the abci listeners, mainly for versiondb support Update CHANGELOG.md Signed-off-by: yihuang cleanup * chore: apply custom-ed store * feat: Append send restrictionto Delegate/Undelegate Coins (#202) * chore: Update go.mod for custom store * chore: fix lint * chore: fix lint * chore: Update go.mod for custom cosmos-sdk/store * Revert "chore: fix lint" This reverts commit 8e7af64fbd9c83f1ad1b49c02ecea7b09ad831af. --------- Signed-off-by: yihuang Co-authored-by: yihuang Co-authored-by: mmsqe Co-authored-by: Kyuhyeon Choi --- baseapp/abci.go | 107 +++++++---- baseapp/baseapp.go | 61 +++++- baseapp/genesis.go | 2 +- baseapp/options.go | 12 +- baseapp/test_helpers.go | 4 +- baseapp/txexecutor.go | 18 ++ core/coins/format.go | 5 + go.mod | 3 + go.sum | 4 +- runtime/module.go | 7 + runtime/store.go | 11 +- server/mock/store.go | 22 +-- simapp/app.go | 6 + simapp/app_config.go | 1 + simapp/go.mod | 3 + simapp/go.sum | 4 +- tests/go.mod | 3 + tests/go.sum | 4 +- .../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 +- types/context.go | 95 ++++++++++ x/bank/keeper/collections_test.go | 4 +- x/bank/keeper/keeper.go | 23 ++- x/bank/keeper/keeper_test.go | 47 ++++- x/bank/keeper/send.go | 15 +- x/bank/keeper/virtual.go | 179 ++++++++++++++++++ x/bank/module.go | 10 +- x/bank/types/keys.go | 3 + x/gov/testutil/expected_keepers_mocks.go | 23 +++ x/group/internal/orm/testsupport.go | 4 +- 38 files changed, 642 insertions(+), 98 deletions(-) create mode 100644 baseapp/txexecutor.go create mode 100644 x/bank/keeper/virtual.go diff --git a/baseapp/abci.go b/baseapp/abci.go index 2bef4642ba41..3d3b6f11926e 100644 --- a/baseapp/abci.go +++ b/baseapp/abci.go @@ -336,11 +336,11 @@ func (app *BaseApp) ApplySnapshotChunk(req *abci.RequestApplySnapshotChunk) (*ab func (app *BaseApp) CheckTx(req *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) { var mode execMode - switch { - case req.Type == abci.CheckTxType_New: + switch req.Type { + case abci.CheckTxType_New: mode = execModeCheck - case req.Type == abci.CheckTxType_Recheck: + case abci.CheckTxType_Recheck: mode = execModeReCheck default: @@ -775,48 +775,48 @@ func (app *BaseApp) internalFinalizeBlock(ctx context.Context, req *abci.Request // Reset the gas meter so that the AnteHandlers aren't required to gasMeter = app.getBlockGasMeter(app.finalizeBlockState.Context()) - app.finalizeBlockState.SetContext(app.finalizeBlockState.Context().WithBlockGasMeter(gasMeter)) + app.finalizeBlockState.SetContext( + app.finalizeBlockState.Context(). + WithBlockGasMeter(gasMeter). + WithTxCount(len(req.Txs)), + ) // Iterate over all raw transactions in the proposal and attempt to execute // them, gathering the execution results. // // NOTE: Not all raw transactions may adhere to the sdk.Tx interface, e.g. // vote extensions, so skip those. - txResults := make([]*abci.ExecTxResult, 0, len(req.Txs)) - for _, rawTx := range req.Txs { - var response *abci.ExecTxResult - - if _, err := app.txDecoder(rawTx); err == nil { - response = app.deliverTx(rawTx) - } else { - // In the case where a transaction included in a block proposal is malformed, - // we still want to return a default response to comet. This is because comet - // expects a response for each transaction included in a block proposal. - response = sdkerrors.ResponseExecTxResultWithEvents( - sdkerrors.ErrTxDecode, - 0, - 0, - nil, - false, - ) - } - - // check after every tx if we should abort - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - // continue - } - - txResults = append(txResults, response) + txResults, err := app.executeTxs(ctx, req.Txs) + if err != nil { + // usually due to canceled + return nil, err } if app.finalizeBlockState.ms.TracingEnabled() { app.finalizeBlockState.ms = app.finalizeBlockState.ms.SetTracingContext(nil).(storetypes.CacheMultiStore) } - endBlock, err := app.endBlock(app.finalizeBlockState.Context()) + var ( + blockGasUsed uint64 + blockGasWanted uint64 + ) + for _, res := range txResults { + // GasUsed should not be -1 but just in case + if res.GasUsed > 0 { + blockGasUsed += uint64(res.GasUsed) + } + // GasWanted could be -1 if the tx is invalid + if res.GasWanted > 0 { + blockGasWanted += uint64(res.GasWanted) + } + } + app.finalizeBlockState.SetContext( + app.finalizeBlockState.Context(). + WithBlockGasUsed(blockGasUsed). + WithBlockGasWanted(blockGasWanted), + ) + + endBlock, err := app.endBlock(ctx) if err != nil { return nil, err } @@ -840,6 +840,45 @@ func (app *BaseApp) internalFinalizeBlock(ctx context.Context, req *abci.Request }, nil } +func (app *BaseApp) executeTxs(ctx context.Context, txs [][]byte) ([]*abci.ExecTxResult, error) { + if app.txExecutor != nil { + return app.txExecutor(ctx, txs, app.finalizeBlockState.ms, func(i int, memTx sdk.Tx, ms storetypes.MultiStore, incarnationCache map[string]any) *abci.ExecTxResult { + return app.deliverTxWithMultiStore(txs[i], memTx, i, ms, incarnationCache) + }) + } + + txResults := make([]*abci.ExecTxResult, 0, len(txs)) + for i, rawTx := range txs { + var response *abci.ExecTxResult + + if memTx, err := app.txDecoder(rawTx); err == nil { + response = app.deliverTx(rawTx, memTx, i) + } else { + // In the case where a transaction included in a block proposal is malformed, + // we still want to return a default response to comet. This is because comet + // expects a response for each transaction included in a block proposal. + response = sdkerrors.ResponseExecTxResultWithEvents( + sdkerrors.ErrTxDecode, + 0, + 0, + nil, + false, + ) + } + + // check after every tx if we should abort + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + // continue + } + + txResults = append(txResults, response) + } + return txResults, nil +} + // FinalizeBlock will execute the block proposal provided by RequestFinalizeBlock. // Specifically, it will execute an application's BeginBlock (if defined), followed // by the transactions in the proposal, finally followed by the application's @@ -1188,7 +1227,7 @@ func (app *BaseApp) CreateQueryContext(height int64, prove bool) (sdk.Context, e // use custom query multi-store if provided qms := app.qms if qms == nil { - qms = app.cms.(storetypes.MultiStore) + qms = storetypes.RootMultiStore(app.cms) } lastBlockHeight := qms.LatestVersion() diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index a89b48bcbafc..d7a4fe91039d 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -65,7 +65,7 @@ type BaseApp struct { name string // application name from abci.BlockInfo db dbm.DB // common DB backend cms storetypes.CommitMultiStore // Main (uncached) state - qms storetypes.MultiStore // Optional alternative multistore for querying only. + qms storetypes.RootMultiStore // Optional alternative multistore for querying only. storeLoader StoreLoader // function to handle store loading, may be overridden with SetStoreLoader() grpcQueryRouter *GRPCQueryRouter // router for redirecting gRPC query calls msgServiceRouter *MsgServiceRouter // router for redirecting Msg service messages @@ -194,6 +194,9 @@ type BaseApp struct { // // SAFETY: it's safe to do if validators validate the total gas wanted in the `ProcessProposal`, which is the case in the default handler. disableBlockGasMeter bool + + // Optional alternative tx executor, used for block-stm parallel transaction execution. + txExecutor TxExecutor } // NewBaseApp returns a reference to an initialized BaseApp. It accepts a @@ -302,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)) } @@ -341,6 +347,17 @@ func (app *BaseApp) MountMemoryStores(keys map[string]*storetypes.MemoryStoreKey } } +// MountObjectStores mounts all transient object stores with the BaseApp's internal +// commit multi-store. +func (app *BaseApp) MountObjectStores(keys map[string]*storetypes.ObjectStoreKey) { + skeys := maps.Keys(keys) + sort.Strings(skeys) + for _, key := range skeys { + memKey := keys[key] + app.MountStore(memKey, storetypes.StoreTypeObject) + } +} + // MountStore mounts a store to the provided key in the BaseApp multistore, // using the default DB. func (app *BaseApp) MountStore(key storetypes.StoreKey, typ storetypes.StoreType) { @@ -659,13 +676,14 @@ func (app *BaseApp) getBlockGasMeter(ctx sdk.Context) storetypes.GasMeter { } // retrieve the context for the tx w/ txBytes and other memoized values. -func (app *BaseApp) getContextForTx(mode execMode, txBytes []byte) sdk.Context { +func (app *BaseApp) getContextForTx(mode execMode, txBytes []byte, txIndex int) sdk.Context { modeState := app.getState(mode) if modeState == nil { panic(fmt.Sprintf("state is nil for mode %v", mode)) } ctx := modeState.Context(). WithTxBytes(txBytes). + WithTxIndex(txIndex). WithGasMeter(storetypes.NewInfiniteGasMeter()) // WithVoteInfos(app.voteInfos) // TODO: identify if this is needed @@ -750,7 +768,11 @@ func (app *BaseApp) beginBlock(_ *abci.RequestFinalizeBlock) (sdk.BeginBlock, er return resp, nil } -func (app *BaseApp) deliverTx(tx []byte) *abci.ExecTxResult { +func (app *BaseApp) deliverTx(tx []byte, memTx sdk.Tx, txIndex int) *abci.ExecTxResult { + return app.deliverTxWithMultiStore(tx, memTx, txIndex, nil, nil) +} + +func (app *BaseApp) deliverTxWithMultiStore(tx []byte, memTx sdk.Tx, txIndex int, txMultiStore storetypes.MultiStore, incarnationCache map[string]any) *abci.ExecTxResult { gInfo := sdk.GasInfo{} resultStr := "successful" @@ -763,7 +785,7 @@ func (app *BaseApp) deliverTx(tx []byte) *abci.ExecTxResult { telemetry.SetGauge(float32(gInfo.GasWanted), "tx", "gas", "wanted") }() - gInfo, result, anteEvents, err := app.runTx(execModeFinalize, tx) + gInfo, result, anteEvents, err := app.runTxWithMultiStore(execModeFinalize, tx, memTx, txIndex, txMultiStore, incarnationCache) if err != nil { resultStr = "failed" resp = sdkerrors.ResponseExecTxResultWithEvents( @@ -821,12 +843,27 @@ func (app *BaseApp) endBlock(_ context.Context) (sdk.EndBlock, error) { // returned if the tx does not run out of gas and if all the messages are valid // and execute successfully. An error is returned otherwise. func (app *BaseApp) runTx(mode execMode, txBytes []byte) (gInfo sdk.GasInfo, result *sdk.Result, anteEvents []abci.Event, err error) { + return app.runTxWithMultiStore(mode, txBytes, nil, -1, nil, nil) +} + +func (app *BaseApp) runTxWithMultiStore( + mode execMode, + txBytes []byte, + tx sdk.Tx, + txIndex int, + txMultiStore storetypes.MultiStore, + incarnationCache map[string]any, +) (gInfo sdk.GasInfo, result *sdk.Result, anteEvents []abci.Event, err error) { // NOTE: GasWanted should be returned by the AnteHandler. GasUsed is // determined by the GasMeter. We need access to the context to get the gas // meter, so we initialize upfront. var gasWanted uint64 - ctx := app.getContextForTx(mode, txBytes) + ctx := app.getContextForTx(mode, txBytes, txIndex) + ctx = ctx.WithIncarnationCache(incarnationCache) + if txMultiStore != nil { + ctx = ctx.WithMultiStore(txMultiStore) + } ms := ctx.MultiStore() // only run the tx if there is block gas remaining @@ -868,9 +905,11 @@ func (app *BaseApp) runTx(mode execMode, txBytes []byte) (gInfo sdk.GasInfo, res defer consumeBlockGas() } - tx, err := app.txDecoder(txBytes) - if err != nil { - return sdk.GasInfo{}, nil, nil, err + if tx == nil { + tx, err = app.txDecoder(txBytes) + if err != nil { + return sdk.GasInfo{}, nil, nil, err + } } msgs := tx.GetMsgs() @@ -1010,6 +1049,8 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, msgsV2 []protov2.Me break } + ctx = ctx.WithMsgIndex(i) + handler := app.msgServiceRouter.Handler(msg) if handler == nil { return nil, errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "no message handler found for %T", msg) @@ -1143,6 +1184,10 @@ func (app *BaseApp) TxEncode(tx sdk.Tx) ([]byte, error) { return app.txEncoder(tx) } +func (app *BaseApp) StreamingManager() storetypes.StreamingManager { + return app.streamingManager +} + // Close is called in start cmd to gracefully cleanup resources. func (app *BaseApp) Close() error { var errs []error diff --git a/baseapp/genesis.go b/baseapp/genesis.go index 4a6b0082b656..562bdca3b276 100644 --- a/baseapp/genesis.go +++ b/baseapp/genesis.go @@ -13,7 +13,7 @@ var _ genesis.TxHandler = (*BaseApp)(nil) // ExecuteGenesisTx implements genesis.GenesisState from // cosmossdk.io/core/genesis to set initial state in genesis func (ba BaseApp) ExecuteGenesisTx(tx []byte) error { - res := ba.deliverTx(tx) + res := ba.deliverTx(tx, nil, -1) if res.Code != types.CodeTypeOK { return errors.New(res.Log) diff --git a/baseapp/options.go b/baseapp/options.go index 7218a283e794..9e066c94c1c4 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -122,6 +122,11 @@ func DisableBlockGasMeter() func(*BaseApp) { return func(app *BaseApp) { app.SetDisableBlockGasMeter(true) } } +// SetTxExecutor sets a custom tx executor for the BaseApp, usually for parallel execution. +func SetTxExecutor(executor TxExecutor) func(*BaseApp) { + return func(app *BaseApp) { app.txExecutor = executor } +} + func (app *BaseApp) SetName(name string) { if app.sealed { panic("SetName() on sealed BaseApp") @@ -314,7 +319,7 @@ func (app *BaseApp) SetTxEncoder(txEncoder sdk.TxEncoder) { // SetQueryMultiStore set a alternative MultiStore implementation to support grpc query service. // // Ref: https://github.com/cosmos/cosmos-sdk/issues/13317 -func (app *BaseApp) SetQueryMultiStore(ms storetypes.MultiStore) { +func (app *BaseApp) SetQueryMultiStore(ms storetypes.RootMultiStore) { app.qms = ms } @@ -378,6 +383,11 @@ func (app *BaseApp) SetDisableBlockGasMeter(disableBlockGasMeter bool) { app.disableBlockGasMeter = disableBlockGasMeter } +// SetTxExecutor sets a custom tx executor for the BaseApp, usually for parallel execution. +func (app *BaseApp) SetTxExecutor(executor TxExecutor) { + app.txExecutor = executor +} + // SetMsgServiceRouter sets the MsgServiceRouter of a BaseApp. func (app *BaseApp) SetMsgServiceRouter(msgServiceRouter *MsgServiceRouter) { app.msgServiceRouter = msgServiceRouter diff --git a/baseapp/test_helpers.go b/baseapp/test_helpers.go index a8f7853f224b..e854517ccd23 100644 --- a/baseapp/test_helpers.go +++ b/baseapp/test_helpers.go @@ -71,9 +71,9 @@ func (app *BaseApp) NewUncachedContext(isCheckTx bool, header cmtproto.Header) s } func (app *BaseApp) GetContextForFinalizeBlock(txBytes []byte) sdk.Context { - return app.getContextForTx(execModeFinalize, txBytes) + return app.getContextForTx(execModeFinalize, txBytes, -1) } func (app *BaseApp) GetContextForCheckTx(txBytes []byte) sdk.Context { - return app.getContextForTx(execModeCheck, txBytes) + return app.getContextForTx(execModeCheck, txBytes, -1) } diff --git a/baseapp/txexecutor.go b/baseapp/txexecutor.go new file mode 100644 index 000000000000..a2dd83f42031 --- /dev/null +++ b/baseapp/txexecutor.go @@ -0,0 +1,18 @@ +package baseapp + +import ( + "context" + + abci "github.com/cometbft/cometbft/abci/types" + + "cosmossdk.io/store/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type TxExecutor func( + ctx context.Context, + block [][]byte, + cms types.MultiStore, + deliverTxWithMultiStore func(int, sdk.Tx, types.MultiStore, map[string]any) *abci.ExecTxResult, +) ([]*abci.ExecTxResult, error) diff --git a/core/coins/format.go b/core/coins/format.go index 315c96d78f46..7b1e49f2b7e2 100644 --- a/core/coins/format.go +++ b/core/coins/format.go @@ -84,6 +84,11 @@ func FormatCoins(coins []*basev1beta1.Coin, metadata []*bankv1beta1.Metadata) (s if err != nil { return "", err } + // If a coin contains a comma, return an error given that the output + // could be misinterpreted by the user as 2 different coins. + if strings.Contains(formatted[i], ",") { + return "", fmt.Errorf("coin %s contains a comma", formatted[i]) + } } if len(coins) == 0 { diff --git a/go.mod b/go.mod index 2ccb17dabd53..bf68c3d62a37 100644 --- a/go.mod +++ b/go.mod @@ -168,6 +168,9 @@ require ( // // ) +// replace the store module with the b-harvest fork +replace cosmossdk.io/store => github.com/b-harvest/cosmos-sdk/store v0.0.0-20241106072527-a25eb3a65854 + // Below are the long-lived replace of the Cosmos SDK replace ( // use cosmos fork of keyring diff --git a/go.sum b/go.sum index 04b8b138b8f8..f91ac0a8fded 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,6 @@ cosmossdk.io/log v1.3.1 h1:UZx8nWIkfbbNEWusZqzAx3ZGvu54TZacWib3EzUYmGI= cosmossdk.io/log v1.3.1/go.mod h1:2/dIomt8mKdk6vl3OWJcPk2be3pGOS8OQaLUM/3/tCM= cosmossdk.io/math v1.3.0 h1:RC+jryuKeytIiictDslBP9i1fhkVm6ZDmZEoNP316zE= cosmossdk.io/math v1.3.0/go.mod h1:vnRTxewy+M7BtXBNFybkuhSH4WfedVAAnERHgVFhp3k= -cosmossdk.io/store v1.1.0 h1:LnKwgYMc9BInn9PhpTFEQVbL9UK475G2H911CGGnWHk= -cosmossdk.io/store v1.1.0/go.mod h1:oZfW/4Fc/zYqu3JmQcQdUJ3fqu5vnYTn3LZFFy8P8ng= cosmossdk.io/x/tx v0.13.3 h1:Ha4mNaHmxBc6RMun9aKuqul8yHiL78EKJQ8g23Zf73g= cosmossdk.io/x/tx v0.13.3/go.mod h1:I8xaHv0rhUdIvIdptKIqzYy27+n2+zBVaxO6fscFhys= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -61,6 +59,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/b-harvest/cosmos-sdk/store v0.0.0-20241106072527-a25eb3a65854 h1:KcAh4n5qOx36uh9hN8nQDdLKHT+Uz8dT6nWN5AW7yi8= +github.com/b-harvest/cosmos-sdk/store v0.0.0-20241106072527-a25eb3a65854/go.mod h1:oZfW/4Fc/zYqu3JmQcQdUJ3fqu5vnYTn3LZFFy8P8ng= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= diff --git a/runtime/module.go b/runtime/module.go index 9ab1bbf40816..6d5a7874a18e 100644 --- a/runtime/module.go +++ b/runtime/module.go @@ -67,6 +67,7 @@ func init() { ProvideKVStoreKey, ProvideTransientStoreKey, ProvideMemoryStoreKey, + ProvideObjectStoreKey, ProvideGenesisTxHandler, ProvideKVStoreService, ProvideMemoryStoreService, @@ -225,6 +226,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/runtime/store.go b/runtime/store.go index 1806ea0d7699..fdd2b1f8ede8 100644 --- a/runtime/store.go +++ b/runtime/store.go @@ -2,9 +2,6 @@ package runtime import ( "context" - "io" - - dbm "github.com/cosmos/cosmos-db" "cosmossdk.io/core/store" storetypes "cosmossdk.io/store/types" @@ -108,10 +105,6 @@ func (kvStoreAdapter) CacheWrap() storetypes.CacheWrap { panic("unimplemented") } -func (kvStoreAdapter) CacheWrapWithTrace(w io.Writer, tc storetypes.TraceContext) storetypes.CacheWrap { - panic("unimplemented") -} - func (kvStoreAdapter) GetStoreType() storetypes.StoreType { panic("unimplemented") } @@ -146,7 +139,7 @@ func (s kvStoreAdapter) Set(key, value []byte) { } } -func (s kvStoreAdapter) Iterator(start, end []byte) dbm.Iterator { +func (s kvStoreAdapter) Iterator(start, end []byte) storetypes.Iterator { it, err := s.store.Iterator(start, end) if err != nil { panic(err) @@ -154,7 +147,7 @@ func (s kvStoreAdapter) Iterator(start, end []byte) dbm.Iterator { return it } -func (s kvStoreAdapter) ReverseIterator(start, end []byte) dbm.Iterator { +func (s kvStoreAdapter) ReverseIterator(start, end []byte) storetypes.Iterator { it, err := s.store.ReverseIterator(start, end) if err != nil { panic(err) diff --git a/server/mock/store.go b/server/mock/store.go index 6e8a30364614..a73c57272f42 100644 --- a/server/mock/store.go +++ b/server/mock/store.go @@ -12,7 +12,7 @@ import ( storetypes "cosmossdk.io/store/types" ) -var _ storetypes.MultiStore = multiStore{} +var _ storetypes.CommitMultiStore = multiStore{} type multiStore struct { kv map[storetypes.StoreKey]kvStore @@ -30,14 +30,6 @@ func (ms multiStore) CacheWrap() storetypes.CacheWrap { panic("not implemented") } -func (ms multiStore) CacheWrapWithTrace(_ io.Writer, _ storetypes.TraceContext) storetypes.CacheWrap { - panic("not implemented") -} - -func (ms multiStore) CacheWrapWithListeners(_ storetypes.StoreKey, _ []storetypes.MemoryListener) storetypes.CacheWrap { - panic("not implemented") -} - func (ms multiStore) TracingEnabled() bool { panic("not implemented") } @@ -114,6 +106,10 @@ func (ms multiStore) GetKVStore(key storetypes.StoreKey) storetypes.KVStore { return ms.kv[key] } +func (ms multiStore) GetObjKVStore(storetypes.StoreKey) storetypes.ObjKVStore { + panic("not implemented") +} + func (ms multiStore) GetStore(key storetypes.StoreKey) storetypes.Store { panic("not implemented") } @@ -178,14 +174,6 @@ func (kv kvStore) CacheWrap() storetypes.CacheWrap { panic("not implemented") } -func (kv kvStore) CacheWrapWithTrace(w io.Writer, tc storetypes.TraceContext) storetypes.CacheWrap { - panic("not implemented") -} - -func (kv kvStore) CacheWrapWithListeners(_ storetypes.StoreKey, _ []storetypes.MemoryListener) storetypes.CacheWrap { - panic("not implemented") -} - func (kv kvStore) GetStoreType() storetypes.StoreType { panic("not implemented") } diff --git a/simapp/app.go b/simapp/app.go index 13db5d04354d..ab804feeee80 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 @@ -265,6 +266,7 @@ func NewSimApp( } tkeys := storetypes.NewTransientStoreKeys(paramstypes.TStoreKey) + okeys := storetypes.NewObjectStoreKeys(banktypes.ObjectStoreKey) app := &SimApp{ BaseApp: bApp, legacyAmino: legacyAmino, @@ -273,6 +275,7 @@ func NewSimApp( interfaceRegistry: interfaceRegistry, keys: keys, tkeys: tkeys, + okeys: okeys, } app.ParamsKeeper = initParamsKeeper(appCodec, legacyAmino, keys[paramstypes.StoreKey], tkeys[paramstypes.TStoreKey]) @@ -287,6 +290,7 @@ func NewSimApp( app.BankKeeper = bankkeeper.NewBaseKeeper( appCodec, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], app.AccountKeeper, BlockedAddresses(), authtypes.NewModuleAddress(govtypes.ModuleName).String(), @@ -455,6 +459,7 @@ func NewSimApp( authz.ModuleName, ) app.ModuleManager.SetOrderEndBlockers( + banktypes.ModuleName, crisistypes.ModuleName, govtypes.ModuleName, stakingtypes.ModuleName, @@ -515,6 +520,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/simapp/go.mod b/simapp/go.mod index 03dac272f95b..8baabc2764e0 100644 --- a/simapp/go.mod +++ b/simapp/go.mod @@ -206,6 +206,9 @@ require ( // // ) +// replace the store module with the b-harvest fork +replace cosmossdk.io/store => github.com/b-harvest/cosmos-sdk/store v0.0.0-20241106072527-a25eb3a65854 + // Below are the long-lived replace of the SimApp replace ( // use cosmos fork of keyring diff --git a/simapp/go.sum b/simapp/go.sum index 538afa422a1d..325386e53534 100644 --- a/simapp/go.sum +++ b/simapp/go.sum @@ -200,8 +200,6 @@ cosmossdk.io/log v1.3.1 h1:UZx8nWIkfbbNEWusZqzAx3ZGvu54TZacWib3EzUYmGI= cosmossdk.io/log v1.3.1/go.mod h1:2/dIomt8mKdk6vl3OWJcPk2be3pGOS8OQaLUM/3/tCM= cosmossdk.io/math v1.3.0 h1:RC+jryuKeytIiictDslBP9i1fhkVm6ZDmZEoNP316zE= cosmossdk.io/math v1.3.0/go.mod h1:vnRTxewy+M7BtXBNFybkuhSH4WfedVAAnERHgVFhp3k= -cosmossdk.io/store v1.1.0 h1:LnKwgYMc9BInn9PhpTFEQVbL9UK475G2H911CGGnWHk= -cosmossdk.io/store v1.1.0/go.mod h1:oZfW/4Fc/zYqu3JmQcQdUJ3fqu5vnYTn3LZFFy8P8ng= cosmossdk.io/tools/confix v0.1.1 h1:aexyRv9+y15veH3Qw16lxQwo+ki7r2I+g0yNTEFEQM8= cosmossdk.io/tools/confix v0.1.1/go.mod h1:nQVvP1tHsGXS83PonPVWJtSbddIqyjEw99L4M3rPJyQ= cosmossdk.io/x/circuit v0.1.1 h1:KPJCnLChWrxD4jLwUiuQaf5mFD/1m7Omyo7oooefBVQ= @@ -262,6 +260,8 @@ github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX github.com/aws/aws-sdk-go v1.44.224 h1:09CiaaF35nRmxrzWZ2uRq5v6Ghg/d2RiPjZnSgtt+RQ= github.com/aws/aws-sdk-go v1.44.224/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/b-harvest/cosmos-sdk/store v0.0.0-20241106072527-a25eb3a65854 h1:KcAh4n5qOx36uh9hN8nQDdLKHT+Uz8dT6nWN5AW7yi8= +github.com/b-harvest/cosmos-sdk/store v0.0.0-20241106072527-a25eb3a65854/go.mod h1:oZfW/4Fc/zYqu3JmQcQdUJ3fqu5vnYTn3LZFFy8P8ng= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= diff --git a/tests/go.mod b/tests/go.mod index 8ed5d8ab19b2..c48f15dcd650 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -204,6 +204,9 @@ require ( // // ) +// replace the store module with the b-harvest fork +replace cosmossdk.io/store => github.com/b-harvest/cosmos-sdk/store v0.0.0-20241106041742-539dfed374c1 + // Below are the long-lived replace for tests. replace ( // We always want to test against the latest version of the simapp. diff --git a/tests/go.sum b/tests/go.sum index 44304d5249b1..d847cb0dcaa7 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -200,8 +200,6 @@ cosmossdk.io/log v1.3.1 h1:UZx8nWIkfbbNEWusZqzAx3ZGvu54TZacWib3EzUYmGI= cosmossdk.io/log v1.3.1/go.mod h1:2/dIomt8mKdk6vl3OWJcPk2be3pGOS8OQaLUM/3/tCM= cosmossdk.io/math v1.3.0 h1:RC+jryuKeytIiictDslBP9i1fhkVm6ZDmZEoNP316zE= cosmossdk.io/math v1.3.0/go.mod h1:vnRTxewy+M7BtXBNFybkuhSH4WfedVAAnERHgVFhp3k= -cosmossdk.io/store v1.1.0 h1:LnKwgYMc9BInn9PhpTFEQVbL9UK475G2H911CGGnWHk= -cosmossdk.io/store v1.1.0/go.mod h1:oZfW/4Fc/zYqu3JmQcQdUJ3fqu5vnYTn3LZFFy8P8ng= cosmossdk.io/x/circuit v0.1.1 h1:KPJCnLChWrxD4jLwUiuQaf5mFD/1m7Omyo7oooefBVQ= cosmossdk.io/x/circuit v0.1.1/go.mod h1:B6f/urRuQH8gjt4eLIXfZJucrbreuYrKh5CSjaOxr+Q= cosmossdk.io/x/evidence v0.1.1 h1:Ks+BLTa3uftFpElLTDp9L76t2b58htjVbSZ86aoK/E4= @@ -260,6 +258,8 @@ github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX github.com/aws/aws-sdk-go v1.44.224 h1:09CiaaF35nRmxrzWZ2uRq5v6Ghg/d2RiPjZnSgtt+RQ= github.com/aws/aws-sdk-go v1.44.224/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/b-harvest/cosmos-sdk/store v0.0.0-20241106041742-539dfed374c1 h1:9Pm/TPkciDhRnXgog07ubQp2kgrkp9D7my90JCfwx+g= +github.com/b-harvest/cosmos-sdk/store v0.0.0-20241106041742-539dfed374c1/go.mod h1:oZfW/4Fc/zYqu3JmQcQdUJ3fqu5vnYTn3LZFFy8P8ng= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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/types/context.go b/types/context.go index a6e304e8d148..fe5a4ded2d61 100644 --- a/types/context.go +++ b/types/context.go @@ -2,6 +2,7 @@ package types import ( "context" + "errors" "time" abci "github.com/cometbft/cometbft/abci/types" @@ -65,6 +66,21 @@ type Context struct { streamingManager storetypes.StreamingManager cometInfo comet.BlockInfo headerInfo header.Info + + // the index of the current tx in the block, -1 means not in finalize block context + txIndex int + // the index of the current msg in the tx, -1 means not in finalize block context + msgIndex int + // the total number of transactions in current block + txCount int + // sum the gas used by all the transactions in the current block, only accessible by end blocker + blockGasUsed uint64 + // sum the gas wanted by all the transactions in the current block, only accessible by end blocker + blockGasWanted uint64 + + // incarnationCache is shared between multiple incarnations of the same transaction, + // it must only cache stateless computation results that only depends on tx body and block level information that don't change during block execution, like the result of tx signature verification. + incarnationCache map[string]any } // Proposed rename, not done to avoid API breakage @@ -93,6 +109,28 @@ func (c Context) TransientKVGasConfig() storetypes.GasConfig { return c.trans func (c Context) StreamingManager() storetypes.StreamingManager { return c.streamingManager } func (c Context) CometInfo() comet.BlockInfo { return c.cometInfo } func (c Context) HeaderInfo() header.Info { return c.headerInfo } +func (c Context) TxIndex() int { return c.txIndex } +func (c Context) MsgIndex() int { return c.msgIndex } +func (c Context) TxCount() int { return c.txCount } +func (c Context) BlockGasUsed() uint64 { return c.blockGasUsed } +func (c Context) BlockGasWanted() uint64 { return c.blockGasWanted } +func (c Context) IncarnationCache() map[string]any { return c.incarnationCache } + +func (c Context) GetIncarnationCache(key string) (any, bool) { + if c.incarnationCache == nil { + return nil, false + } + val, ok := c.incarnationCache[key] + return val, ok +} + +func (c Context) SetIncarnationCache(key string, value any) { + if c.incarnationCache == nil { + // noop if cache is not initialized + return + } + c.incarnationCache[key] = value +} // clone the header before returning func (c Context) BlockHeader() cmtproto.Header { @@ -140,6 +178,8 @@ func NewContext(ms storetypes.MultiStore, header cmtproto.Header, isCheckTx bool eventManager: NewEventManager(), kvGasConfig: storetypes.KVGasConfig(), transientKVGasConfig: storetypes.TransientGasConfig(), + txIndex: -1, + msgIndex: -1, } } @@ -319,6 +359,36 @@ func (c Context) WithHeaderInfo(headerInfo header.Info) Context { return c } +func (c Context) WithTxIndex(txIndex int) Context { + c.txIndex = txIndex + return c +} + +func (c Context) WithTxCount(txCount int) Context { + c.txCount = txCount + return c +} + +func (c Context) WithMsgIndex(msgIndex int) Context { + c.msgIndex = msgIndex + return c +} + +func (c Context) WithBlockGasUsed(gasUsed uint64) Context { + c.blockGasUsed = gasUsed + return c +} + +func (c Context) WithBlockGasWanted(gasWanted uint64) Context { + c.blockGasWanted = gasWanted + return c +} + +func (c Context) WithIncarnationCache(cache map[string]any) Context { + c.incarnationCache = cache + return c +} + // TODO: remove??? func (c Context) IsZero() bool { return c.ms == nil @@ -351,6 +421,11 @@ func (c Context) TransientStore(key storetypes.StoreKey) storetypes.KVStore { return gaskv.NewStore(c.ms.GetKVStore(key), c.gasMeter, c.transientKVGasConfig) } +// ObjectStore fetches an object store from the MultiStore, +func (c Context) ObjectStore(key storetypes.StoreKey) storetypes.ObjKVStore { + return gaskv.NewObjStore(c.ms.GetObjKVStore(key), c.gasMeter, c.transientKVGasConfig) +} + // CacheContext returns a new Context with the multi-store cached and a new // EventManager. The cached context is written to the context when writeCache // is called. Note, events are automatically emitted on the parent context's @@ -367,6 +442,26 @@ func (c Context) CacheContext() (cc Context, writeCache func()) { return cc, writeCache } +// RunAtomic execute the callback function atomically, i.e. the state and event changes are +// only persisted if the callback returns no error, or discarded as a whole. +// It uses an efficient approach than CacheContext, without wrapping stores. +func (c Context) RunAtomic(cb func(Context) error) error { + evtManager := NewEventManager() + cacheMS, ok := c.ms.(storetypes.CacheMultiStore) + if !ok { + return errors.New("multistore is not a CacheMultiStore") + } + if err := cacheMS.RunAtomic(func(ms storetypes.CacheMultiStore) error { + ctx := c.WithMultiStore(ms).WithEventManager(evtManager) + return cb(ctx) + }); err != nil { + return err + } + + c.EventManager().EmitEvents(evtManager.Events()) + return nil +} + var ( _ context.Context = Context{} _ storetypes.Context = Context{} 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..257e45ad1396 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, @@ -131,6 +138,11 @@ func (k BaseKeeper) DelegateCoins(ctx context.Context, delegatorAddr, moduleAccA return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, amt.String()) } + moduleAccAddr, err := k.sendRestriction.apply(ctx, delegatorAddr, moduleAccAddr, amt) + if err != nil { + return err + } + balances := sdk.NewCoins() for _, coin := range amt { @@ -157,7 +169,7 @@ func (k BaseKeeper) DelegateCoins(ctx context.Context, delegatorAddr, moduleAccA types.NewCoinSpentEvent(delegatorAddr, amt), ) - err := k.addCoins(ctx, moduleAccAddr, amt) + err = k.addCoins(ctx, moduleAccAddr, amt) if err != nil { return err } @@ -180,7 +192,12 @@ func (k BaseKeeper) UndelegateCoins(ctx context.Context, moduleAccAddr, delegato return errorsmod.Wrap(sdkerrors.ErrInvalidCoins, amt.String()) } - err := k.subUnlockedCoins(ctx, moduleAccAddr, amt) + delegatorAddr, err := k.sendRestriction.apply(ctx, moduleAccAddr, delegatorAddr, amt) + if err != nil { + return err + } + + err = k.subUnlockedCoins(ctx, moduleAccAddr, amt) if err != nil { return err } 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 c8aad5446f3b..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) @@ -247,8 +260,6 @@ func (k BaseSendKeeper) SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccA 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..08e37803fa5d --- /dev/null +++ b/x/bank/keeper/virtual.go @@ -0,0 +1,179 @@ +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 abf0e4d9ef94..091729a88b7e 100644 --- a/x/bank/module.go +++ b/x/bank/module.go @@ -16,6 +16,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. @@ -193,6 +195,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() diff --git a/x/group/internal/orm/testsupport.go b/x/group/internal/orm/testsupport.go index b4fe3d0354ee..b03fcec42ba9 100644 --- a/x/group/internal/orm/testsupport.go +++ b/x/group/internal/orm/testsupport.go @@ -26,8 +26,8 @@ func NewMockContext() *MockContext { } func (m MockContext) KVStore(key storetypes.StoreKey) storetypes.KVStore { - if s := m.store.GetCommitKVStore(key); s != nil { - return s + if s := m.store.GetCommitStore(key); s != nil { + return s.(storetypes.KVStore) } m.store.MountStoreWithDB(key, storetypes.StoreTypeIAVL, m.db) if err := m.store.LoadLatestVersion(); err != nil {